diff --git a/core/libs/jpegutils/jpegutils.cpp b/core/libs/jpegutils/jpegutils.cpp index 5a0e270734..6f1378aeeb 100644 --- a/core/libs/jpegutils/jpegutils.cpp +++ b/core/libs/jpegutils/jpegutils.cpp @@ -1,1018 +1,1018 @@ /* ============================================================ * * 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 * Copyright (C) 2006-2020 by Gilles Caulier * Copyright (C) 2006-2012 by Marcel Wiesweg * * 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 #include // C ANSI includes extern "C" { #include #include #include } // 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 #include #include #include #include // 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(cinfo->err); char buffer[JMSG_LENGTH_MAX]; (*cinfo->err->format_message)(cinfo, buffer); qCDebug(DIGIKAM_GENERAL_LOG) << "Jpegutils error, aborting operation:" << buffer; longjmp(myerr->setjmp_buffer, 1); } 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; if (setjmp(jerr.setjmp_buffer)) { 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(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(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.load(file); m_orientation = m_metadata.getItemOrientation(); QFileInfo info(file); m_documentName = info.fileName(); } // ----------------------------------------------------------------------------- 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 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) { SafeTemporaryFile* const temp = new SafeTemporaryFile(dir + QLatin1String("/JpegRotator-XXXXXX.digikamtempfile.jpg")); temp->setAutoRemove(false); temp->open(); - QString tempFile = temp->safeFileName(); + 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. DMetadata meta(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 diff --git a/core/libs/threadimageio/engine/filereadwritelock.cpp b/core/libs/threadimageio/engine/filereadwritelock.cpp index 5ae88b3363..67681ba245 100644 --- a/core/libs/threadimageio/engine/filereadwritelock.cpp +++ b/core/libs/threadimageio/engine/filereadwritelock.cpp @@ -1,523 +1,523 @@ /* ============================================================ * * This file is a part of digiKam project * https://www.digikam.org * * Date : 2012-01-29 * Description : Intra-process file i/o lock * * Copyright (C) 2012 by Marcel Wiesweg * * Parts of this file are based on qreadwritelock.cpp, LGPL, * Copyright (C) 2011 Nokia Corporation and/or its subsidiary(-ies). * * 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. * * ============================================================ */ #include "filereadwritelock.h" // Qt includes #include #include #include #include #include #include // Local includes #include "digikam_debug.h" namespace Digikam { class Q_DECL_HIDDEN FileReadWriteLockPriv { public: explicit FileReadWriteLockPriv(const QString& filePath) : filePath(filePath), ref(0), waitingReaders(0), waitingWriters(0), accessCount(0), writer(nullptr) { } bool isFree() const { return (readers.isEmpty() && !writer && !waitingReaders && !waitingWriters); } public: QString filePath; int ref; int waitingReaders; int waitingWriters; int accessCount; Qt::HANDLE writer; QHash readers; }; typedef FileReadWriteLockPriv Entry; class Q_DECL_HIDDEN FileReadWriteLockStaticPrivate { public: QMutex mutex; QWaitCondition readerWait; QWaitCondition writerWait; QMutex tempFileMutex; QHash entries; public: Entry* entry(const QString& filePath); void drop(Entry* entry); void lockForRead(Entry* entry); void lockForWrite(Entry* entry); bool tryLockForRead(Entry* entry); bool tryLockForRead(Entry* entry, int timeout); bool tryLockForWrite(Entry* entry); bool tryLockForWrite(Entry* entry, int timeout); void unlock(Entry* entry); Entry* entryLockedForRead(const QString& filePath); Entry* entryLockedForWrite(const QString& filePath); void unlockAndDrop(Entry* entry); private: Entry* entry_locked(const QString& filePath); void drop_locked(Entry* entry); bool lockForRead_locked(Entry* entry, int mode, int timeout); bool lockForWrite_locked(Entry* entry, int mode, int timeout); void unlock_locked(Entry* entry); }; // --- Entry allocation --- Entry* FileReadWriteLockStaticPrivate::entry(const QString& filePath) { QMutexLocker lock(&mutex); return entry_locked(filePath); } Entry* FileReadWriteLockStaticPrivate::entry_locked(const QString& filePath) { QHash::iterator it = entries.find(filePath); if (it == entries.end()) { it = entries.insert(filePath, new Entry(filePath)); } (*it)->ref++; return *it; } void FileReadWriteLockStaticPrivate::drop(Entry* entry) { QMutexLocker lock(&mutex); drop_locked(entry); } void FileReadWriteLockStaticPrivate::drop_locked(Entry* entry) { entry->ref--; if (entry->ref == 0 && entry->isFree()) { entries.remove(entry->filePath); delete entry; } } // --- locking implementation --- void FileReadWriteLockStaticPrivate::lockForRead(Entry* entry) { QMutexLocker lock(&mutex); lockForRead_locked(entry, 0, 0); } bool FileReadWriteLockStaticPrivate::tryLockForRead(Entry* entry) { QMutexLocker lock(&mutex); return lockForRead_locked(entry, 1, 0); } bool FileReadWriteLockStaticPrivate::tryLockForRead(Entry* entry, int timeout) { QMutexLocker lock(&mutex); return lockForRead_locked(entry, 2, timeout); } bool FileReadWriteLockStaticPrivate::lockForRead_locked(Entry* entry, int mode, int timeout) { Qt::HANDLE self = QThread::currentThreadId(); // already recursively write-locked by this thread? if (entry->writer == self) { // If we already have the write lock recursively, just add another write lock instead of the read lock. // This situation is clean and all right. --entry->accessCount; return true; } // recursive read lock by this thread? QHash::iterator it = entry->readers.find(self); if (it != entry->readers.end()) { ++it.value(); ++entry->accessCount; return true; } if (mode == 1) { // tryLock if (entry->accessCount < 0) { return false; } } else { while (entry->accessCount < 0 || entry->waitingWriters) { if (mode == 2) { // tryLock with timeout ++entry->waitingReaders; bool success = readerWait.wait(&mutex, timeout); --entry->waitingReaders; if (!success) { return false; } } else { // lock ++entry->waitingReaders; readerWait.wait(&mutex); --entry->waitingReaders; } } } entry->readers.insert(self, 1); ++entry->accessCount; return true; } void FileReadWriteLockStaticPrivate::lockForWrite(Entry* entry) { QMutexLocker lock(&mutex); lockForWrite_locked(entry, 0, 0); } bool FileReadWriteLockStaticPrivate::tryLockForWrite(Entry* entry) { QMutexLocker lock(&mutex); return lockForWrite_locked(entry, 1, 0); } bool FileReadWriteLockStaticPrivate::tryLockForWrite(Entry* entry, int timeout) { QMutexLocker lock(&mutex); return lockForRead_locked(entry, 2, timeout); } bool FileReadWriteLockStaticPrivate::lockForWrite_locked(Entry* entry, int mode, int timeout) { Qt::HANDLE self = QThread::currentThreadId(); // recursive write-lock by this thread? if (entry->writer == self) { --entry->accessCount; return true; } // recursive read lock by this thread? QHash::iterator it = entry->readers.find(self); int recursiveReadLockCount = 0; if (it != entry->readers.end()) { // We could deadlock, or promote the read locks to write locks qCWarning(DIGIKAM_GENERAL_LOG) << "Locking for write, recursively locked for read: Promoting existing read locks to write locks! " << "Avoid this situation."; // The lock was locked for read it.value() times by this thread recursively recursiveReadLockCount = it.value(); entry->accessCount -= it.value(); entry->readers.erase(it); } while (entry->accessCount != 0) { if (mode == 1) { // tryLock return false; } else if (mode == 2) { // tryLock with timeout entry->waitingWriters++; bool success = writerWait.wait(&mutex, timeout); entry->waitingWriters--; if (!success) { return false; } } else { // lock entry->waitingWriters++; writerWait.wait(&mutex); entry->waitingWriters--; } } entry->writer = self; --entry->accessCount; // if we had recursive read locks, they are now promoted to write locks entry->accessCount -= recursiveReadLockCount; return true; } void FileReadWriteLockStaticPrivate::unlock(Entry* entry) { QMutexLocker lock(&mutex); unlock_locked(entry); } void FileReadWriteLockStaticPrivate::unlock_locked(Entry* entry) { bool unlocked = false; if (entry->accessCount > 0) { // releasing a read lock Qt::HANDLE self = QThread::currentThreadId(); QHash::iterator it = entry->readers.find(self); if (it != entry->readers.end()) { if (--it.value() <= 0) { entry->readers.erase(it); } } unlocked = --entry->accessCount == 0; } else if ((entry->accessCount < 0) && (++entry->accessCount == 0)) { // released a write lock unlocked = true; entry->writer = nullptr; } if (unlocked) { if (entry->waitingWriters) { // we must wake all as it is one wait condition for all entries writerWait.wakeAll(); } else if (entry->waitingReaders) { readerWait.wakeAll(); } } } // --- Combination methods --- Entry* FileReadWriteLockStaticPrivate::entryLockedForRead(const QString& filePath) { QMutexLocker lock(&mutex); Entry* e = entry_locked(filePath); lockForRead_locked(e, 0, 0); return e; } Entry* FileReadWriteLockStaticPrivate::entryLockedForWrite(const QString& filePath) { QMutexLocker lock(&mutex); Entry* e = entry_locked(filePath); lockForWrite_locked(e, 0, 0); return e; } void FileReadWriteLockStaticPrivate::unlockAndDrop(Entry* entry) { QMutexLocker lock(&mutex); unlock_locked(entry); drop_locked(entry); } Q_GLOBAL_STATIC(FileReadWriteLockStaticPrivate, static_d) // ------------------------------------------------------------------------- FileReadWriteLockKey::FileReadWriteLockKey(const QString& filePath) : d(static_d->entry(filePath)) { } FileReadWriteLockKey::~FileReadWriteLockKey() { static_d->drop(d); } void FileReadWriteLockKey::lockForRead() { static_d->lockForRead(d); } void FileReadWriteLockKey::lockForWrite() { static_d->lockForWrite(d); } bool FileReadWriteLockKey::tryLockForRead() { return static_d->tryLockForRead(d); } bool FileReadWriteLockKey::tryLockForRead(int timeout) { return static_d->tryLockForRead(d, timeout); } bool FileReadWriteLockKey::tryLockForWrite() { return static_d->tryLockForWrite(d); } bool FileReadWriteLockKey::tryLockForWrite(int timeout) { return static_d->tryLockForWrite(d, timeout); } void FileReadWriteLockKey::unlock() { return static_d->unlock(d); } // ------------------------------------------------------------------------- FileReadLocker::FileReadLocker(const QString& filePath) : d(static_d->entryLockedForRead(filePath)) { } FileReadLocker::~FileReadLocker() { static_d->unlockAndDrop(d); } FileWriteLocker::FileWriteLocker(const QString& filePath) : d(static_d->entryLockedForWrite(filePath)) { } FileWriteLocker::~FileWriteLocker() { static_d->unlockAndDrop(d); } // ------------------------------------------------------------------------- SafeTemporaryFile::SafeTemporaryFile() { } SafeTemporaryFile::SafeTemporaryFile(const QString& templ) : QTemporaryFile(templ), m_templ(templ) { } bool SafeTemporaryFile::open(QIODevice::OpenMode mode) { QMutexLocker lock(&static_d->tempFileMutex); return QTemporaryFile::open(mode); } // Workaround for Qt-Bug 74291 with UNC paths -QString SafeTemporaryFile::safeFileName() const +QString SafeTemporaryFile::safeFilePath() const { QFileInfo orgInfo(m_templ); QFileInfo tmpInfo(fileName()); return (orgInfo.path() + QLatin1Char('/') + tmpInfo.fileName()); } } // namespace Digikam diff --git a/core/libs/threadimageio/engine/filereadwritelock.h b/core/libs/threadimageio/engine/filereadwritelock.h index 7e3fa245f4..1c3b27b840 100644 --- a/core/libs/threadimageio/engine/filereadwritelock.h +++ b/core/libs/threadimageio/engine/filereadwritelock.h @@ -1,116 +1,116 @@ /* ============================================================ * * This file is a part of digiKam project * https://www.digikam.org * * Date : 2012-01-29 * Description : Intra-process file i/o lock * * Copyright (C) 2012 by Marcel Wiesweg * * 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. * * ============================================================ */ #ifndef DIGIKAM_FILE_READ_WRITE_LOCK_H #define DIGIKAM_FILE_READ_WRITE_LOCK_H // Qt includes #include #include // Local includes #include "digikam_export.h" namespace Digikam { class FileReadWriteLockPriv; class DIGIKAM_EXPORT FileReadWriteLockKey { public: explicit FileReadWriteLockKey(const QString& filePath); ~FileReadWriteLockKey(); void lockForRead(); void lockForWrite(); bool tryLockForRead(); bool tryLockForRead(int timeout); bool tryLockForWrite(); bool tryLockForWrite(int timeout); void unlock(); private: FileReadWriteLockPriv* d; }; // ---------------------------------------------------------------------- class DIGIKAM_EXPORT FileReadLocker { public: explicit FileReadLocker(const QString& filePath); ~FileReadLocker(); private: FileReadWriteLockPriv* d; }; // ---------------------------------------------------------------------- class DIGIKAM_EXPORT FileWriteLocker { public: explicit FileWriteLocker(const QString& filePath); ~FileWriteLocker(); private: FileReadWriteLockPriv* d; }; // ---------------------------------------------------------------------- class DIGIKAM_EXPORT SafeTemporaryFile : public QTemporaryFile { public: explicit SafeTemporaryFile(const QString& templ); SafeTemporaryFile(); bool open() { return open(QIODevice::ReadWrite); }; - QString safeFileName() const; + QString safeFilePath() const; protected: virtual bool open(QIODevice::OpenMode) override; private: QString m_templ; }; } // namespace Digikam #endif // DIGIKAM_FILE_READ_WRITE_LOCK_H diff --git a/core/utilities/imageeditor/editor/editorwindow.cpp b/core/utilities/imageeditor/editor/editorwindow.cpp index ac85ca22c7..9e2844c511 100644 --- a/core/utilities/imageeditor/editor/editorwindow.cpp +++ b/core/utilities/imageeditor/editor/editorwindow.cpp @@ -1,2698 +1,2698 @@ /* ============================================================ * * This file is a part of digiKam project * https://www.digikam.org * * Date : 2006-01-20 * Description : core image editor GUI implementation * * Copyright (C) 2006-2020 by Gilles Caulier * Copyright (C) 2009-2011 by Andi Clemens * Copyright (C) 2015 by Mohamed_Anwer * * 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. * * ============================================================ */ #include "editorwindow_p.h" namespace Digikam { EditorWindow::EditorWindow(const QString& name) : DXmlGuiWindow(nullptr), d(new Private) { setConfigGroupName(QLatin1String("ImageViewer Settings")); setObjectName(name); setWindowFlags(Qt::Window); setFullScreenOptions(FS_EDITOR); m_nonDestructive = true; m_contextMenu = nullptr; m_servicesMenu = nullptr; m_serviceAction = nullptr; m_canvas = nullptr; m_openVersionAction = nullptr; m_saveAction = nullptr; m_saveAsAction = nullptr; m_saveCurrentVersionAction = nullptr; m_saveNewVersionAction = nullptr; m_saveNewVersionAsAction = nullptr; m_saveNewVersionInFormatAction = nullptr; m_resLabel = nullptr; m_nameLabel = nullptr; m_exportAction = nullptr; m_revertAction = nullptr; m_discardChangesAction = nullptr; m_fileDeleteAction = nullptr; m_forwardAction = nullptr; m_backwardAction = nullptr; m_firstAction = nullptr; m_lastAction = nullptr; m_applyToolAction = nullptr; m_closeToolAction = nullptr; m_undoAction = nullptr; m_redoAction = nullptr; m_showBarAction = nullptr; m_splitter = nullptr; m_vSplitter = nullptr; m_stackView = nullptr; m_setExifOrientationTag = true; m_editingOriginalImage = true; m_actionEnabledState = false; // Settings containers instance. d->exposureSettings = new ExposureSettingsContainer(); d->toolIface = new EditorToolIface(this); m_IOFileSettings = new IOFileSettings(); //d->waitingLoop = new QEventLoop(this); } EditorWindow::~EditorWindow() { delete m_canvas; delete m_IOFileSettings; delete d->toolIface; delete d->exposureSettings; delete d; } SidebarSplitter* EditorWindow::sidebarSplitter() const { return m_splitter; } EditorStackView* EditorWindow::editorStackView() const { return m_stackView; } ExposureSettingsContainer* EditorWindow::exposureSettings() const { return d->exposureSettings; } void EditorWindow::setupContextMenu() { m_contextMenu = new QMenu(this); addAction2ContextMenu(QLatin1String("editorwindow_fullscreen"), true); addAction2ContextMenu(QLatin1String("options_show_menubar"), true); m_contextMenu->addSeparator(); // -------------------------------------------------------- addAction2ContextMenu(QLatin1String("editorwindow_backward"), true); addAction2ContextMenu(QLatin1String("editorwindow_forward"), true); m_contextMenu->addSeparator(); // -------------------------------------------------------- addAction2ContextMenu(QLatin1String("slideshow_plugin"), true); addAction2ContextMenu(QLatin1String("editorwindow_transform_rotateleft"), true); addAction2ContextMenu(QLatin1String("editorwindow_transform_rotateright"), true); addAction2ContextMenu(QLatin1String("editorwindow_transform_crop"), true); m_contextMenu->addSeparator(); // -------------------------------------------------------- addAction2ContextMenu(QLatin1String("editorwindow_delete"), true); } void EditorWindow::setupStandardConnections() { connect(m_stackView, SIGNAL(signalToggleOffFitToWindow()), this, SLOT(slotToggleOffFitToWindow())); // -- Canvas connections ------------------------------------------------ connect(m_canvas, SIGNAL(signalShowNextImage()), this, SLOT(slotForward())); connect(m_canvas, SIGNAL(signalShowPrevImage()), this, SLOT(slotBackward())); connect(m_canvas, SIGNAL(signalRightButtonClicked()), this, SLOT(slotContextMenu())); connect(m_stackView, SIGNAL(signalZoomChanged(bool,bool,double)), this, SLOT(slotZoomChanged(bool,bool,double))); connect(m_canvas, SIGNAL(signalChanged()), this, SLOT(slotChanged())); connect(m_canvas, SIGNAL(signalAddedDropedItems(QDropEvent*)), this, SLOT(slotAddedDropedItems(QDropEvent*))); connect(m_canvas->interface(), SIGNAL(signalUndoStateChanged()), this, SLOT(slotUndoStateChanged())); connect(m_canvas, SIGNAL(signalSelected(bool)), this, SLOT(slotSelected(bool))); connect(m_canvas, SIGNAL(signalPrepareToLoad()), this, SLOT(slotPrepareToLoad())); connect(m_canvas, SIGNAL(signalLoadingStarted(QString)), this, SLOT(slotLoadingStarted(QString))); connect(m_canvas, SIGNAL(signalLoadingFinished(QString,bool)), this, SLOT(slotLoadingFinished(QString,bool))); connect(m_canvas, SIGNAL(signalLoadingProgress(QString,float)), this, SLOT(slotLoadingProgress(QString,float))); connect(m_canvas, SIGNAL(signalSavingStarted(QString)), this, SLOT(slotSavingStarted(QString))); connect(m_canvas, SIGNAL(signalSavingFinished(QString,bool)), this, SLOT(slotSavingFinished(QString,bool))); connect(m_canvas, SIGNAL(signalSavingProgress(QString,float)), this, SLOT(slotSavingProgress(QString,float))); connect(m_canvas, SIGNAL(signalSelectionChanged(QRect)), this, SLOT(slotSelectionChanged(QRect))); connect(m_canvas, SIGNAL(signalSelectionSetText(QRect)), this, SLOT(slotSelectionSetText(QRect))); connect(m_canvas->interface(), SIGNAL(signalFileOriginChanged(QString)), this, SLOT(slotFileOriginChanged(QString))); // -- status bar connections -------------------------------------- connect(m_nameLabel, SIGNAL(signalCancelButtonPressed()), this, SLOT(slotNameLabelCancelButtonPressed())); connect(m_nameLabel, SIGNAL(signalCancelButtonPressed()), d->toolIface, SLOT(slotToolAborted())); // -- Icc settings connections -------------------------------------- connect(IccSettings::instance(), SIGNAL(settingsChanged()), this, SLOT(slotColorManagementOptionsChanged())); } void EditorWindow::setupStandardActions() { // -- Standard 'File' menu actions --------------------------------------------- KActionCollection* const ac = actionCollection(); m_backwardAction = buildStdAction(StdBackAction, this, SLOT(slotBackward()), this); ac->addAction(QLatin1String("editorwindow_backward"), m_backwardAction); ac->setDefaultShortcuts(m_backwardAction, QList() << Qt::Key_PageUp << Qt::Key_Backspace << Qt::Key_Up << Qt::Key_Left); m_backwardAction->setEnabled(false); m_forwardAction = buildStdAction(StdForwardAction, this, SLOT(slotForward()), this); ac->addAction(QLatin1String("editorwindow_forward"), m_forwardAction); ac->setDefaultShortcuts(m_forwardAction, QList() << Qt::Key_PageDown << Qt::Key_Space << Qt::Key_Down << Qt::Key_Right); m_forwardAction->setEnabled(false); m_firstAction = new QAction(QIcon::fromTheme(QLatin1String("go-first")), i18n("&First"), this); connect(m_firstAction, SIGNAL(triggered()), this, SLOT(slotFirst())); ac->addAction(QLatin1String("editorwindow_first"), m_firstAction); ac->setDefaultShortcuts(m_firstAction, QList() << Qt::CTRL + Qt::Key_Home); m_firstAction->setEnabled(false); m_lastAction = new QAction(QIcon::fromTheme(QLatin1String("go-last")), i18n("&Last"), this); connect(m_lastAction, SIGNAL(triggered()), this, SLOT(slotLast())); ac->addAction(QLatin1String("editorwindow_last"), m_lastAction); ac->setDefaultShortcuts(m_lastAction, QList() << Qt::CTRL + Qt::Key_End); m_lastAction->setEnabled(false); m_openVersionAction = new QAction(QIcon::fromTheme(QLatin1String("view-preview")), i18nc("@action", "Open Original"), this); connect(m_openVersionAction, SIGNAL(triggered()), this, SLOT(slotOpenOriginal())); ac->addAction(QLatin1String("editorwindow_openversion"), m_openVersionAction); ac->setDefaultShortcuts(m_openVersionAction, QList() << Qt::CTRL + Qt::Key_End); m_saveAction = buildStdAction(StdSaveAction, this, SLOT(saveOrSaveAs()), this); ac->addAction(QLatin1String("editorwindow_save"), m_saveAction); m_saveAsAction = buildStdAction(StdSaveAsAction, this, SLOT(saveAs()), this); ac->addAction(QLatin1String("editorwindow_saveas"), m_saveAsAction); m_saveCurrentVersionAction = new QAction(QIcon::fromTheme(QLatin1String("dialog-ok-apply")), i18nc("@action Save changes to current version", "Save Changes"), this); m_saveCurrentVersionAction->setToolTip(i18nc("@info:tooltip", "Save the modifications to the current version of the file")); connect(m_saveCurrentVersionAction, SIGNAL(triggered()), this, SLOT(saveCurrentVersion())); ac->addAction(QLatin1String("editorwindow_savecurrentversion"), m_saveCurrentVersionAction); m_saveNewVersionAction = new KToolBarPopupAction(QIcon::fromTheme(QLatin1String("list-add")), i18nc("@action Save changes to a newly created version", "Save As New Version"), this); m_saveNewVersionAction->setToolTip(i18nc("@info:tooltip", "Save the current modifications to a new version of the file")); connect(m_saveNewVersionAction, SIGNAL(triggered()), this, SLOT(saveNewVersion())); ac->addAction(QLatin1String("editorwindow_savenewversion"), m_saveNewVersionAction); QAction* const m_saveNewVersionAsAction = new QAction(QIcon::fromTheme(QLatin1String("document-save-as")), i18nc("@action Save changes to a newly created version, specifying the filename and format", "Save New Version As..."), this); m_saveNewVersionAsAction->setToolTip(i18nc("@info:tooltip", "Save the current modifications to a new version of the file, " "specifying the filename and format")); connect(m_saveNewVersionAsAction, SIGNAL(triggered()), this, SLOT(saveNewVersionAs())); m_saveNewVersionInFormatAction = new QMenu(i18nc("@action Save As New Version...Save in format...", "Save in Format"), this); m_saveNewVersionInFormatAction->setIcon(QIcon::fromTheme(QLatin1String("view-preview"))); d->plugNewVersionInFormatAction(this, m_saveNewVersionInFormatAction, i18nc("@action:inmenu", "JPEG"), QLatin1String("JPG")); d->plugNewVersionInFormatAction(this, m_saveNewVersionInFormatAction, i18nc("@action:inmenu", "TIFF"), QLatin1String("TIFF")); d->plugNewVersionInFormatAction(this, m_saveNewVersionInFormatAction, i18nc("@action:inmenu", "PNG"), QLatin1String("PNG")); d->plugNewVersionInFormatAction(this, m_saveNewVersionInFormatAction, i18nc("@action:inmenu", "PGF"), QLatin1String("PGF")); #ifdef HAVE_JASPER d->plugNewVersionInFormatAction(this, m_saveNewVersionInFormatAction, i18nc("@action:inmenu", "JPEG 2000"), QLatin1String("JP2")); #endif // HAVE_JASPER #ifdef HAVE_X265 d->plugNewVersionInFormatAction(this, m_saveNewVersionInFormatAction, i18nc("@action:inmenu", "HEIC"), QLatin1String("HEIC")); #endif // HAVE_X265 m_saveNewVersionAction->menu()->addAction(m_saveNewVersionAsAction); m_saveNewVersionAction->menu()->addAction(m_saveNewVersionInFormatAction->menuAction()); // This also triggers saveAs, but in the context of non-destructive we want a slightly different appearance m_exportAction = new QAction(QIcon::fromTheme(QLatin1String("document-export")), i18nc("@action", "Export"), this); m_exportAction->setToolTip(i18nc("@info:tooltip", "Save the file in a folder outside your collection")); connect(m_exportAction, SIGNAL(triggered()), this, SLOT(saveAs())); ac->addAction(QLatin1String("editorwindow_export"), m_exportAction); ac->setDefaultShortcut(m_exportAction, Qt::CTRL + Qt::SHIFT + Qt::Key_E); // NOTE: Gimp shortcut m_revertAction = buildStdAction(StdRevertAction, this, SLOT(slotRevert()), this); ac->addAction(QLatin1String("editorwindow_revert"), m_revertAction); m_discardChangesAction = new QAction(QIcon::fromTheme(QLatin1String("task-reject")), i18nc("@action", "Discard Changes"), this); m_discardChangesAction->setToolTip(i18nc("@info:tooltip", "Discard all current changes to this file")); connect(m_discardChangesAction, SIGNAL(triggered()), this, SLOT(slotDiscardChanges())); ac->addAction(QLatin1String("editorwindow_discardchanges"), m_discardChangesAction); m_openVersionAction->setEnabled(false); m_saveAction->setEnabled(false); m_saveAsAction->setEnabled(false); m_saveCurrentVersionAction->setEnabled(false); m_saveNewVersionAction->setEnabled(false); m_revertAction->setEnabled(false); m_discardChangesAction->setEnabled(false); d->openWithAction = new QAction(QIcon::fromTheme(QLatin1String("preferences-desktop-filetype-association")), i18n("Open With Default Application"), this); d->openWithAction->setWhatsThis(i18n("Open the item with default assigned application.")); connect(d->openWithAction, SIGNAL(triggered()), this, SLOT(slotFileWithDefaultApplication())); ac->addAction(QLatin1String("open_with_default_application"), d->openWithAction); ac->setDefaultShortcut(d->openWithAction, Qt::CTRL + Qt::Key_F4); d->openWithAction->setEnabled(false); m_fileDeleteAction = new QAction(QIcon::fromTheme(QLatin1String("user-trash-full")), i18nc("Non-pluralized", "Move to Trash"), this); connect(m_fileDeleteAction, SIGNAL(triggered()), this, SLOT(slotDeleteCurrentItem())); ac->addAction(QLatin1String("editorwindow_delete"), m_fileDeleteAction); ac->setDefaultShortcut(m_fileDeleteAction, Qt::Key_Delete); m_fileDeleteAction->setEnabled(false); QAction* const closeAction = buildStdAction(StdCloseAction, this, SLOT(close()), this); ac->addAction(QLatin1String("editorwindow_close"), closeAction); // -- Standard 'Edit' menu actions --------------------------------------------- d->copyAction = buildStdAction(StdCopyAction, m_canvas, SLOT(slotCopy()), this); ac->addAction(QLatin1String("editorwindow_copy"), d->copyAction); d->copyAction->setEnabled(false); m_undoAction = new KToolBarPopupAction(QIcon::fromTheme(QLatin1String("edit-undo")), i18n("Undo"), this); m_undoAction->setEnabled(false); ac->addAction(QLatin1String("editorwindow_undo"), m_undoAction); ac->setDefaultShortcuts(m_undoAction, QList() << Qt::CTRL + Qt::Key_Z); connect(m_undoAction->menu(), SIGNAL(aboutToShow()), this, SLOT(slotAboutToShowUndoMenu())); // connect simple undo action connect(m_undoAction, &QAction::triggered, this, [this]() { m_canvas->slotUndo(1); }); m_redoAction = new KToolBarPopupAction(QIcon::fromTheme(QLatin1String("edit-redo")), i18n("Redo"), this); m_redoAction->setEnabled(false); ac->addAction(QLatin1String("editorwindow_redo"), m_redoAction); ac->setDefaultShortcuts(m_redoAction, QList() << Qt::CTRL + Qt::SHIFT + Qt::Key_Z); connect(m_redoAction->menu(), SIGNAL(aboutToShow()), this, SLOT(slotAboutToShowRedoMenu())); // connect simple redo action connect(m_redoAction, &QAction::triggered, this, [this]() { m_canvas->slotRedo(1); }); d->selectAllAction = new QAction(i18nc("Create a selection containing the full image", "Select All"), this); connect(d->selectAllAction, SIGNAL(triggered()), m_canvas, SLOT(slotSelectAll())); ac->addAction(QLatin1String("editorwindow_selectAll"), d->selectAllAction); ac->setDefaultShortcut(d->selectAllAction, Qt::CTRL + Qt::Key_A); d->selectNoneAction = new QAction(i18n("Select None"), this); connect(d->selectNoneAction, SIGNAL(triggered()), m_canvas, SLOT(slotSelectNone())); ac->addAction(QLatin1String("editorwindow_selectNone"), d->selectNoneAction); ac->setDefaultShortcut(d->selectNoneAction, Qt::CTRL + Qt::SHIFT + Qt::Key_A); // -- Standard 'View' menu actions --------------------------------------------- d->zoomPlusAction = buildStdAction(StdZoomInAction, this, SLOT(slotIncreaseZoom()), this); ac->addAction(QLatin1String("editorwindow_zoomplus"), d->zoomPlusAction); d->zoomMinusAction = buildStdAction(StdZoomOutAction, this, SLOT(slotDecreaseZoom()), this); ac->addAction(QLatin1String("editorwindow_zoomminus"), d->zoomMinusAction); d->zoomTo100percents = new QAction(QIcon::fromTheme(QLatin1String("zoom-original")), i18n("Zoom to 100%"), this); connect(d->zoomTo100percents, SIGNAL(triggered()), this, SLOT(slotZoomTo100Percents())); ac->addAction(QLatin1String("editorwindow_zoomto100percents"), d->zoomTo100percents); ac->setDefaultShortcut(d->zoomTo100percents, Qt::CTRL + Qt::Key_Period); d->zoomFitToWindowAction = new QAction(QIcon::fromTheme(QLatin1String("zoom-fit-best")), i18n("Fit to &Window"), this); d->zoomFitToWindowAction->setCheckable(true); connect(d->zoomFitToWindowAction, SIGNAL(triggered()), this, SLOT(slotToggleFitToWindow())); ac->addAction(QLatin1String("editorwindow_zoomfit2window"), d->zoomFitToWindowAction); ac->setDefaultShortcut(d->zoomFitToWindowAction, Qt::ALT + Qt::CTRL + Qt::Key_E); d->zoomFitToSelectAction = new QAction(QIcon::fromTheme(QLatin1String("zoom-select-fit")), i18n("Fit to &Selection"), this); connect(d->zoomFitToSelectAction, SIGNAL(triggered()), this, SLOT(slotFitToSelect())); ac->addAction(QLatin1String("editorwindow_zoomfit2select"), d->zoomFitToSelectAction); ac->setDefaultShortcut(d->zoomFitToSelectAction, Qt::ALT + Qt::CTRL + Qt::Key_S); // NOTE: Photoshop 7 use ALT+CTRL+0 d->zoomFitToSelectAction->setEnabled(false); d->zoomFitToSelectAction->setWhatsThis(i18n("This option can be used to zoom the image to the " "current selection area.")); // ********************************************************** QList actions = DPluginLoader::instance()->pluginsActions(DPluginAction::Generic, this); foreach (DPluginAction* const ac, actions) { ac->setEnabled(false); } QList actions2 = DPluginLoader::instance()->pluginsActions(DPluginAction::Editor, this); foreach (DPluginAction* const ac, actions2) { ac->setEnabled(false); } // -------------------------------------------------------- createFullScreenAction(QLatin1String("editorwindow_fullscreen")); createSidebarActions(); d->viewUnderExpoAction = new QAction(QIcon::fromTheme(QLatin1String("underexposure")), i18n("Under-Exposure Indicator"), this); d->viewUnderExpoAction->setCheckable(true); d->viewUnderExpoAction->setWhatsThis(i18n("Set this option to display black " "overlaid on the image. This will help you to avoid " "under-exposing the image.")); connect(d->viewUnderExpoAction, SIGNAL(triggered(bool)), this, SLOT(slotSetUnderExposureIndicator(bool))); ac->addAction(QLatin1String("editorwindow_underexposure"), d->viewUnderExpoAction); ac->setDefaultShortcut(d->viewUnderExpoAction, Qt::Key_F10); d->viewOverExpoAction = new QAction(QIcon::fromTheme(QLatin1String("overexposure")), i18n("Over-Exposure Indicator"), this); d->viewOverExpoAction->setCheckable(true); d->viewOverExpoAction->setWhatsThis(i18n("Set this option to display white " "overlaid on the image. This will help you to avoid " "over-exposing the image.")); connect(d->viewOverExpoAction, SIGNAL(triggered(bool)), this, SLOT(slotSetOverExposureIndicator(bool))); ac->addAction(QLatin1String("editorwindow_overexposure"), d->viewOverExpoAction); ac->setDefaultShortcut(d->viewOverExpoAction, Qt::Key_F11); d->viewCMViewAction = new QAction(QIcon::fromTheme(QLatin1String("video-display")), i18n("Color-Managed View"), this); d->viewCMViewAction->setCheckable(true); connect(d->viewCMViewAction, SIGNAL(triggered()), this, SLOT(slotToggleColorManagedView())); ac->addAction(QLatin1String("editorwindow_cmview"), d->viewCMViewAction); ac->setDefaultShortcut(d->viewCMViewAction, Qt::Key_F12); d->softProofOptionsAction = new QAction(QIcon::fromTheme(QLatin1String("printer")), i18n("Soft Proofing Options..."), this); connect(d->softProofOptionsAction, SIGNAL(triggered()), this, SLOT(slotSoftProofingOptions())); ac->addAction(QLatin1String("editorwindow_softproofoptions"), d->softProofOptionsAction); d->viewSoftProofAction = new QAction(QIcon::fromTheme(QLatin1String("document-print-preview")), i18n("Soft Proofing View"), this); d->viewSoftProofAction->setCheckable(true); connect(d->viewSoftProofAction, SIGNAL(triggered()), this, SLOT(slotUpdateSoftProofingState())); ac->addAction(QLatin1String("editorwindow_softproofview"), d->viewSoftProofAction); // -- Standard 'Transform' menu actions --------------------------------------------- d->cropAction = new QAction(QIcon::fromTheme(QLatin1String("transform-crop-and-resize")), i18nc("@action", "Crop to Selection"), this); connect(d->cropAction, SIGNAL(triggered()), m_canvas, SLOT(slotCrop())); d->cropAction->setEnabled(false); d->cropAction->setWhatsThis(i18n("This option can be used to crop the image. " "Select a region of the image to enable this action.")); ac->addAction(QLatin1String("editorwindow_transform_crop"), d->cropAction); ac->setDefaultShortcut(d->cropAction, Qt::CTRL + Qt::Key_X); // -- Standard 'Flip' menu actions --------------------------------------------- d->flipHorizAction = new QAction(QIcon::fromTheme(QLatin1String("object-flip-horizontal")), i18n("Flip Horizontally"), this); connect(d->flipHorizAction, SIGNAL(triggered()), m_canvas, SLOT(slotFlipHoriz())); connect(d->flipHorizAction, SIGNAL(triggered()), this, SLOT(slotFlipHIntoQue())); ac->addAction(QLatin1String("editorwindow_transform_fliphoriz"), d->flipHorizAction); ac->setDefaultShortcut(d->flipHorizAction, Qt::CTRL + Qt::Key_Asterisk); d->flipHorizAction->setEnabled(false); d->flipVertAction = new QAction(QIcon::fromTheme(QLatin1String("object-flip-vertical")), i18n("Flip Vertically"), this); connect(d->flipVertAction, SIGNAL(triggered()), m_canvas, SLOT(slotFlipVert())); connect(d->flipVertAction, SIGNAL(triggered()), this, SLOT(slotFlipVIntoQue())); ac->addAction(QLatin1String("editorwindow_transform_flipvert"), d->flipVertAction); ac->setDefaultShortcut(d->flipVertAction, Qt::CTRL + Qt::Key_Slash); d->flipVertAction->setEnabled(false); // -- Standard 'Rotate' menu actions ---------------------------------------- d->rotateLeftAction = new QAction(QIcon::fromTheme(QLatin1String("object-rotate-left")), i18n("Rotate Left"), this); connect(d->rotateLeftAction, SIGNAL(triggered()), m_canvas, SLOT(slotRotate270())); connect(d->rotateLeftAction, SIGNAL(triggered()), this, SLOT(slotRotateLeftIntoQue())); ac->addAction(QLatin1String("editorwindow_transform_rotateleft"), d->rotateLeftAction); ac->setDefaultShortcut(d->rotateLeftAction, Qt::CTRL + Qt::SHIFT + Qt::Key_Left); d->rotateLeftAction->setEnabled(false); d->rotateRightAction = new QAction(QIcon::fromTheme(QLatin1String("object-rotate-right")), i18n("Rotate Right"), this); connect(d->rotateRightAction, SIGNAL(triggered()), m_canvas, SLOT(slotRotate90())); connect(d->rotateRightAction, SIGNAL(triggered()), this, SLOT(slotRotateRightIntoQue())); ac->addAction(QLatin1String("editorwindow_transform_rotateright"), d->rotateRightAction); ac->setDefaultShortcut(d->rotateRightAction, Qt::CTRL + Qt::SHIFT + Qt::Key_Right); d->rotateRightAction->setEnabled(false); m_showBarAction = thumbBar()->getToggleAction(this); ac->addAction(QLatin1String("editorwindow_showthumbs"), m_showBarAction); ac->setDefaultShortcut(m_showBarAction, Qt::CTRL + Qt::Key_T); // Provides a menu entry that allows showing/hiding the toolbar(s) setStandardToolBarMenuEnabled(true); // Provides a menu entry that allows showing/hiding the statusbar createStandardStatusBarAction(); // Standard 'Configure' menu actions createSettingsActions(); // --------------------------------------------------------------------------------- ThemeManager::instance()->registerThemeActions(this); connect(ThemeManager::instance(), SIGNAL(signalThemeChanged()), this, SLOT(slotThemeChanged())); // -- Keyboard-only actions -------------------------------------------------------- QAction* const altBackwardAction = new QAction(i18n("Previous Image"), this); ac->addAction(QLatin1String("editorwindow_backward_shift_space"), altBackwardAction); ac->setDefaultShortcut(altBackwardAction, Qt::SHIFT + Qt::Key_Space); connect(altBackwardAction, SIGNAL(triggered()), this, SLOT(slotBackward())); // -- Tool control actions --------------------------------------------------------- m_applyToolAction = new QAction(QIcon::fromTheme(QLatin1String("dialog-ok-apply")), i18n("OK"), this); ac->addAction(QLatin1String("editorwindow_applytool"), m_applyToolAction); ac->setDefaultShortcut(m_applyToolAction, Qt::Key_Return); connect(m_applyToolAction, SIGNAL(triggered()), this, SLOT(slotApplyTool())); m_closeToolAction = new QAction(QIcon::fromTheme(QLatin1String("dialog-cancel")), i18n("Cancel"), this); ac->addAction(QLatin1String("editorwindow_closetool"), m_closeToolAction); ac->setDefaultShortcut(m_closeToolAction, Qt::Key_Escape); connect(m_closeToolAction, SIGNAL(triggered()), this, SLOT(slotCloseTool())); toggleNonDestructiveActions(); toggleToolActions(); } void EditorWindow::setupStatusBar() { m_nameLabel = new StatusProgressBar(statusBar()); m_nameLabel->setAlignment(Qt::AlignCenter); statusBar()->addWidget(m_nameLabel, 100); d->infoLabel = new DAdjustableLabel(statusBar()); d->infoLabel->setAdjustedText(i18n("No selection")); d->infoLabel->setAlignment(Qt::AlignCenter); statusBar()->addWidget(d->infoLabel, 100); d->infoLabel->setToolTip(i18n("Information about current image selection")); m_resLabel = new DAdjustableLabel(statusBar()); m_resLabel->setAlignment(Qt::AlignCenter); statusBar()->addWidget(m_resLabel, 100); m_resLabel->setToolTip(i18n("Information about image size")); d->zoomBar = new DZoomBar(statusBar()); d->zoomBar->setZoomToFitAction(d->zoomFitToWindowAction); d->zoomBar->setZoomTo100Action(d->zoomTo100percents); d->zoomBar->setZoomPlusAction(d->zoomPlusAction); d->zoomBar->setZoomMinusAction(d->zoomMinusAction); d->zoomBar->setBarMode(DZoomBar::PreviewZoomCtrl); statusBar()->addPermanentWidget(d->zoomBar); connect(d->zoomBar, SIGNAL(signalZoomSliderChanged(int)), m_stackView, SLOT(slotZoomSliderChanged(int))); connect(d->zoomBar, SIGNAL(signalZoomValueEdited(double)), m_stackView, SLOT(setZoomFactor(double))); d->previewToolBar = new PreviewToolBar(statusBar()); d->previewToolBar->registerMenuActionGroup(this); d->previewToolBar->setEnabled(false); statusBar()->addPermanentWidget(d->previewToolBar); connect(d->previewToolBar, SIGNAL(signalPreviewModeChanged(int)), this, SIGNAL(signalPreviewModeChanged(int))); QWidget* const buttonsBox = new QWidget(statusBar()); QHBoxLayout* const hlay = new QHBoxLayout(buttonsBox); QButtonGroup* const buttonsGrp = new QButtonGroup(buttonsBox); buttonsGrp->setExclusive(false); d->underExposureIndicator = new QToolButton(buttonsBox); d->underExposureIndicator->setDefaultAction(d->viewUnderExpoAction); d->underExposureIndicator->setFocusPolicy(Qt::NoFocus); d->overExposureIndicator = new QToolButton(buttonsBox); d->overExposureIndicator->setDefaultAction(d->viewOverExpoAction); d->overExposureIndicator->setFocusPolicy(Qt::NoFocus); d->cmViewIndicator = new QToolButton(buttonsBox); d->cmViewIndicator->setDefaultAction(d->viewCMViewAction); d->cmViewIndicator->setFocusPolicy(Qt::NoFocus); buttonsGrp->addButton(d->underExposureIndicator); buttonsGrp->addButton(d->overExposureIndicator); buttonsGrp->addButton(d->cmViewIndicator); hlay->setSpacing(0); hlay->setContentsMargins(QMargins()); hlay->addWidget(d->underExposureIndicator); hlay->addWidget(d->overExposureIndicator); hlay->addWidget(d->cmViewIndicator); statusBar()->addPermanentWidget(buttonsBox); } void EditorWindow::slotAboutToShowUndoMenu() { m_undoAction->menu()->clear(); QStringList titles = m_canvas->interface()->getUndoHistory(); for (int i = 0; i < titles.size(); ++i) { QAction* const action = m_undoAction->menu()->addAction(titles.at(i)); int id = i + 1; connect(action, &QAction::triggered, this, [this, id]() { m_canvas->slotUndo(id); }); } } void EditorWindow::slotAboutToShowRedoMenu() { m_redoAction->menu()->clear(); QStringList titles = m_canvas->interface()->getRedoHistory(); for (int i = 0; i < titles.size(); ++i) { QAction* const action = m_redoAction->menu()->addAction(titles.at(i)); int id = i + 1; connect(action, &QAction::triggered, this, [this, id]() { m_canvas->slotRedo(id); }); } } void EditorWindow::slotIncreaseZoom() { m_stackView->increaseZoom(); } void EditorWindow::slotDecreaseZoom() { m_stackView->decreaseZoom(); } void EditorWindow::slotToggleFitToWindow() { d->zoomPlusAction->setEnabled(true); d->zoomBar->setEnabled(true); d->zoomMinusAction->setEnabled(true); m_stackView->toggleFitToWindow(); } void EditorWindow::slotFitToSelect() { d->zoomPlusAction->setEnabled(true); d->zoomBar->setEnabled(true); d->zoomMinusAction->setEnabled(true); m_stackView->fitToSelect(); } void EditorWindow::slotZoomTo100Percents() { d->zoomPlusAction->setEnabled(true); d->zoomBar->setEnabled(true); d->zoomMinusAction->setEnabled(true); m_stackView->zoomTo100Percent(); } void EditorWindow::slotZoomChanged(bool isMax, bool isMin, double zoom) { //qCDebug(DIGIKAM_GENERAL_LOG) << "EditorWindow::slotZoomChanged"; d->zoomPlusAction->setEnabled(!isMax); d->zoomMinusAction->setEnabled(!isMin); double zmin = m_stackView->zoomMin(); double zmax = m_stackView->zoomMax(); d->zoomBar->setZoom(zoom, zmin, zmax); } void EditorWindow::slotToggleOffFitToWindow() { d->zoomFitToWindowAction->blockSignals(true); d->zoomFitToWindowAction->setChecked(false); d->zoomFitToWindowAction->blockSignals(false); } void EditorWindow::readStandardSettings() { KSharedConfig::Ptr config = KSharedConfig::openConfig(); KConfigGroup group = config->group(configGroupName()); // Restore Canvas layout if (group.hasKey(d->configVerticalSplitterSizesEntry) && m_vSplitter) { QByteArray state; state = group.readEntry(d->configVerticalSplitterStateEntry, state); m_vSplitter->restoreState(QByteArray::fromBase64(state)); } // Restore full screen Mode readFullScreenSettings(group); // Restore Auto zoom action bool autoZoom = group.readEntry(d->configAutoZoomEntry, true); if (autoZoom) { d->zoomFitToWindowAction->trigger(); } slotSetUnderExposureIndicator(group.readEntry(d->configUnderExposureIndicatorEntry, false)); slotSetOverExposureIndicator(group.readEntry(d->configOverExposureIndicatorEntry, false)); d->previewToolBar->readSettings(group); } void EditorWindow::applyStandardSettings() { applyColorManagementSettings(); d->toolIface->updateICCSettings(); applyIOSettings(); // -- GUI Settings ------------------------------------------------------- KConfigGroup group = KSharedConfig::openConfig()->group(configGroupName()); d->legacyUpdateSplitterState(group); m_splitter->restoreState(group); readFullScreenSettings(group); slotThemeChanged(); // -- Exposure Indicators Settings --------------------------------------- d->exposureSettings->underExposureColor = group.readEntry(d->configUnderExposureColorEntry, QColor(Qt::white)); d->exposureSettings->underExposurePercent = group.readEntry(d->configUnderExposurePercentsEntry, 1.0); d->exposureSettings->overExposureColor = group.readEntry(d->configOverExposureColorEntry, QColor(Qt::black)); d->exposureSettings->overExposurePercent = group.readEntry(d->configOverExposurePercentsEntry, 1.0); d->exposureSettings->exposureIndicatorMode = group.readEntry(d->configExpoIndicatorModeEntry, true); d->toolIface->updateExposureSettings(); // -- Metadata Settings -------------------------------------------------- MetaEngineSettingsContainer writeSettings = MetaEngineSettings::instance()->settings(); m_setExifOrientationTag = writeSettings.exifSetOrientation; m_canvas->setExifOrient(writeSettings.exifRotate); } void EditorWindow::applyIOSettings() { // -- JPEG, PNG, TIFF, JPEG2000, PGF files format settings ---------------- KConfigGroup group = KSharedConfig::openConfig()->group(configGroupName()); m_IOFileSettings->JPEGCompression = JPEGSettings::convertCompressionForLibJpeg(group.readEntry(d->configJpegCompressionEntry, 75)); m_IOFileSettings->JPEGSubSampling = group.readEntry(d->configJpegSubSamplingEntry, 1); // Medium subsampling m_IOFileSettings->PNGCompression = PNGSettings::convertCompressionForLibPng(group.readEntry(d->configPngCompressionEntry, 1)); // TIFF compression setting. m_IOFileSettings->TIFFCompression = group.readEntry(d->configTiffCompressionEntry, false); // JPEG2000 quality slider settings : 1 - 100 m_IOFileSettings->JPEG2000Compression = group.readEntry(d->configJpeg2000CompressionEntry, 100); // JPEG2000 LossLess setting. m_IOFileSettings->JPEG2000LossLess = group.readEntry(d->configJpeg2000LossLessEntry, true); // PGF quality slider settings : 1 - 9 m_IOFileSettings->PGFCompression = group.readEntry(d->configPgfCompressionEntry, 3); // PGF LossLess setting. m_IOFileSettings->PGFLossLess = group.readEntry(d->configPgfLossLessEntry, true); // -- RAW images decoding settings ------------------------------------------------------ m_IOFileSettings->useRAWImport = group.readEntry(d->configUseRawImportToolEntry, false); m_IOFileSettings->rawImportToolIid = group.readEntry(d->configRawImportToolIidEntry, QString::fromLatin1("org.kde.digikam.plugin.rawimport.Native")); DRawDecoderWidget::readSettings(m_IOFileSettings->rawDecodingSettings.rawPrm, group); // Raw Color Management settings: // If digiKam Color Management is enabled, no need to correct color of decoded RAW image, // else, sRGB color workspace will be used. ICCSettingsContainer settings = IccSettings::instance()->settings(); if (settings.enableCM) { if (settings.defaultUncalibratedBehavior & ICCSettingsContainer::AutomaticColors) { m_IOFileSettings->rawDecodingSettings.rawPrm.outputColorSpace = DRawDecoderSettings::CUSTOMOUTPUTCS; m_IOFileSettings->rawDecodingSettings.rawPrm.outputProfile = settings.workspaceProfile; } else { m_IOFileSettings->rawDecodingSettings.rawPrm.outputColorSpace = DRawDecoderSettings::RAWCOLOR; } } else { m_IOFileSettings->rawDecodingSettings.rawPrm.outputColorSpace = DRawDecoderSettings::SRGB; } } void EditorWindow::applyColorManagementSettings() { ICCSettingsContainer settings = IccSettings::instance()->settings(); d->toolIface->updateICCSettings(); m_canvas->setICCSettings(settings); d->viewCMViewAction->blockSignals(true); d->viewCMViewAction->setEnabled(settings.enableCM); d->viewCMViewAction->setChecked(settings.useManagedView); setColorManagedViewIndicatorToolTip(settings.enableCM, settings.useManagedView); d->viewCMViewAction->blockSignals(false); d->viewSoftProofAction->setEnabled(settings.enableCM && !settings.defaultProofProfile.isEmpty()); d->softProofOptionsAction->setEnabled(settings.enableCM); } void EditorWindow::saveStandardSettings() { KSharedConfig::Ptr config = KSharedConfig::openConfig(); KConfigGroup group = config->group(configGroupName()); group.writeEntry(d->configAutoZoomEntry, d->zoomFitToWindowAction->isChecked()); m_splitter->saveState(group); if (m_vSplitter) { group.writeEntry(d->configVerticalSplitterStateEntry, m_vSplitter->saveState().toBase64()); } group.writeEntry("Show Thumbbar", thumbBar()->shouldBeVisible()); group.writeEntry(d->configUnderExposureIndicatorEntry, d->exposureSettings->underExposureIndicator); group.writeEntry(d->configOverExposureIndicatorEntry, d->exposureSettings->overExposureIndicator); d->previewToolBar->writeSettings(group); config->sync(); } /** Method used by Editor Tools. Only tools based on imageregionwidget support zooming. TODO: Fix this behavior when editor tool preview widgets will be factored. */ void EditorWindow::toggleZoomActions(bool val) { d->zoomMinusAction->setEnabled(val); d->zoomPlusAction->setEnabled(val); d->zoomTo100percents->setEnabled(val); d->zoomFitToWindowAction->setEnabled(val); d->zoomBar->setEnabled(val); } void EditorWindow::readSettings() { readStandardSettings(); } void EditorWindow::saveSettings() { saveStandardSettings(); } void EditorWindow::toggleActions(bool val) { toggleStandardActions(val); } bool EditorWindow::actionEnabledState() const { return m_actionEnabledState; } void EditorWindow::toggleStandardActions(bool val) { d->zoomFitToSelectAction->setEnabled(val); toggleZoomActions(val); m_actionEnabledState = val; m_forwardAction->setEnabled(val); m_backwardAction->setEnabled(val); m_firstAction->setEnabled(val); m_lastAction->setEnabled(val); d->rotateLeftAction->setEnabled(val); d->rotateRightAction->setEnabled(val); d->flipHorizAction->setEnabled(val); d->flipVertAction->setEnabled(val); m_fileDeleteAction->setEnabled(val); m_saveAsAction->setEnabled(val); d->openWithAction->setEnabled(val); m_exportAction->setEnabled(val); d->selectAllAction->setEnabled(val); d->selectNoneAction->setEnabled(val); QList actions = DPluginLoader::instance()->pluginsActions(DPluginAction::Generic, this); foreach (DPluginAction* const ac, actions) { ac->setEnabled(val); } // these actions are special: They are turned off if val is false, // but if val is true, they may be turned on or off. if (val) { // Update actions by retrieving current values slotUndoStateChanged(); } else { m_openVersionAction->setEnabled(false); m_revertAction->setEnabled(false); m_saveAction->setEnabled(false); m_saveCurrentVersionAction->setEnabled(false); m_saveNewVersionAction->setEnabled(false); m_discardChangesAction->setEnabled(false); m_undoAction->setEnabled(false); m_redoAction->setEnabled(false); } // Editor Tools actions QList actions2 = DPluginLoader::instance()->pluginsActions(DPluginAction::Editor, this); foreach (DPluginAction* const ac, actions2) { ac->setEnabled(val); } } void EditorWindow::toggleNonDestructiveActions() { m_saveAction->setVisible(!m_nonDestructive); m_saveAsAction->setVisible(!m_nonDestructive); m_revertAction->setVisible(!m_nonDestructive); m_openVersionAction->setVisible(m_nonDestructive); m_saveCurrentVersionAction->setVisible(m_nonDestructive); m_saveNewVersionAction->setVisible(m_nonDestructive); m_exportAction->setVisible(m_nonDestructive); m_discardChangesAction->setVisible(m_nonDestructive); } void EditorWindow::toggleToolActions(EditorTool* tool) { if (tool) { m_applyToolAction->setText(tool->toolSettings()->button(EditorToolSettings::Ok)->text()); m_applyToolAction->setIcon(tool->toolSettings()->button(EditorToolSettings::Ok)->icon()); m_applyToolAction->setToolTip(tool->toolSettings()->button(EditorToolSettings::Ok)->toolTip()); m_closeToolAction->setText(tool->toolSettings()->button(EditorToolSettings::Cancel)->text()); m_closeToolAction->setIcon(tool->toolSettings()->button(EditorToolSettings::Cancel)->icon()); m_closeToolAction->setToolTip(tool->toolSettings()->button(EditorToolSettings::Cancel)->toolTip()); } m_applyToolAction->setVisible(tool); m_closeToolAction->setVisible(tool); } void EditorWindow::slotLoadingProgress(const QString&, float progress) { m_nameLabel->setProgressValue((int)(progress * 100.0)); } void EditorWindow::slotSavingProgress(const QString&, float progress) { m_nameLabel->setProgressValue((int)(progress * 100.0)); if (m_savingProgressDialog) { m_savingProgressDialog->setValue((int)(progress * 100.0)); } } void EditorWindow::execSavingProgressDialog() { if (m_savingProgressDialog) { return; } m_savingProgressDialog = new QProgressDialog(this); m_savingProgressDialog->setWindowTitle(i18n("Saving image...")); m_savingProgressDialog->setLabelText(i18n("Please wait for the image to be saved...")); m_savingProgressDialog->setAttribute(Qt::WA_DeleteOnClose); m_savingProgressDialog->setAutoClose(true); m_savingProgressDialog->setMinimumDuration(1000); m_savingProgressDialog->setMaximum(100); // we must enter a fully modal dialog, no QEventLoop is sufficient for KWin to accept longer waiting times m_savingProgressDialog->setModal(true); m_savingProgressDialog->exec(); } bool EditorWindow::promptForOverWrite() { QUrl destination = saveDestinationUrl(); if (destination.isLocalFile()) { QFileInfo fi(m_canvas->currentImageFilePath()); QString warnMsg(i18n("About to overwrite file \"%1\"\nAre you sure?", QDir::toNativeSeparators(fi.fileName()))); return (DMessageBox::showContinueCancel(QMessageBox::Warning, this, i18n("Warning"), warnMsg, QLatin1String("editorWindowSaveOverwrite")) == QMessageBox::Yes); } else { // in this case it will handle the overwrite request return true; } } void EditorWindow::slotUndoStateChanged() { UndoState state = m_canvas->interface()->undoState(); // RAW conversion qualifies as a "non-undoable" action // You can save as new version, but cannot undo or revert m_undoAction->setEnabled(state.hasUndo); m_redoAction->setEnabled(state.hasRedo); m_revertAction->setEnabled(state.hasUndoableChanges); m_saveAction->setEnabled(state.hasChanges); m_saveCurrentVersionAction->setEnabled(state.hasChanges); m_saveNewVersionAction->setEnabled(state.hasChanges); m_discardChangesAction->setEnabled(state.hasUndoableChanges); m_openVersionAction->setEnabled(hasOriginalToRestore()); } bool EditorWindow::hasOriginalToRestore() { return m_canvas->interface()->getResolvedInitialHistory().hasOriginalReferredImage(); } DImageHistory EditorWindow::resolvedImageHistory(const DImageHistory& history) { // simple, database-less version DImageHistory r = history; QList::iterator it; for (it = r.entries().begin(); it != r.entries().end(); ++it) { QList::iterator hit; for (hit = it->referredImages.begin(); hit != it->referredImages.end();) { QFileInfo info(hit->m_filePath + QLatin1Char('/') + hit->m_fileName); if (!info.exists()) { hit = it->referredImages.erase(hit); } else { ++hit; } } } return r; } bool EditorWindow::promptUserSave(const QUrl& url, SaveAskMode mode, bool allowCancel) { if (d->currentWindowModalDialog) { d->currentWindowModalDialog->reject(); } if (m_canvas->interface()->undoState().hasUndoableChanges) { // if window is minimized, show it if (isMinimized()) { KWindowSystem::unminimizeWindow(winId()); } bool shallSave = true; bool shallDiscard = false; bool newVersion = false; if (mode == AskIfNeeded) { if (m_nonDestructive) { if (versionManager()->settings().editorClosingMode == VersionManagerSettings::AutoSave) { shallSave = true; } else { QPointer dialog = new VersioningPromptUserSaveDialog(this); dialog->exec(); if (!dialog) { return false; } shallSave = dialog->shallSave() || dialog->newVersion(); shallDiscard = dialog->shallDiscard(); newVersion = dialog->newVersion(); } } else { QString boxMessage; boxMessage = i18nc("@info", "The image %1 has been modified.
" "Do you want to save it?
", url.fileName()); int result; if (allowCancel) { result = QMessageBox::warning(this, qApp->applicationName(), boxMessage, QMessageBox::Save | QMessageBox::Discard | QMessageBox::Cancel); } else { result = QMessageBox::warning(this, qApp->applicationName(), boxMessage, QMessageBox::Save | QMessageBox::Discard); } shallSave = (result == QMessageBox::Save); shallDiscard = (result == QMessageBox::Discard); } } if (shallSave) { bool saving = false; switch (mode) { case AskIfNeeded: if (m_nonDestructive) { if (newVersion) { saving = saveNewVersion(); } else { // will know on its own if new version is required saving = saveCurrentVersion(); } } else { if (m_canvas->isReadOnly()) { saving = saveAs(); } else if (promptForOverWrite()) { saving = save(); } } break; case OverwriteWithoutAsking: if (m_nonDestructive) { if (newVersion) { saving = saveNewVersion(); } else { // will know on its own if new version is required saving = saveCurrentVersion(); } } else { if (m_canvas->isReadOnly()) { saving = saveAs(); } else { saving = save(); } } break; case AlwaysSaveAs: if (m_nonDestructive) { saving = saveNewVersion(); } else { saving = saveAs(); } break; } // save and saveAs return false if they were canceled and did not enter saving at all // In this case, do not call enterWaitingLoop because quitWaitingloop will not be called. if (saving) { // Waiting for asynchronous image file saving operation running in separate thread. m_savingContext.synchronizingState = SavingContext::SynchronousSaving; enterWaitingLoop(); m_savingContext.synchronizingState = SavingContext::NormalSaving; return m_savingContext.synchronousSavingResult; } else { return false; } } else if (shallDiscard) { // Discard m_saveAction->setEnabled(false); return true; } else { return false; } } return true; } bool EditorWindow::promptUserDelete(const QUrl& url) { if (d->currentWindowModalDialog) { d->currentWindowModalDialog->reject(); } if (m_canvas->interface()->undoState().hasUndoableChanges) { // if window is minimized, show it if (isMinimized()) { KWindowSystem::unminimizeWindow(winId()); } QString boxMessage = i18nc("@info", "The image %1 has been modified.
" "All changes will be lost.", url.fileName()); int result = DMessageBox::showContinueCancel(QMessageBox::Warning, this, QString(), boxMessage); if (result == QMessageBox::Cancel) { return false; } } return true; } bool EditorWindow::waitForSavingToComplete() { // avoid reentrancy - return false means we have reentered the loop already. if (m_savingContext.synchronizingState == SavingContext::SynchronousSaving) { return false; } if (m_savingContext.savingState != SavingContext::SavingStateNone) { // Waiting for asynchronous image file saving operation running in separate thread. m_savingContext.synchronizingState = SavingContext::SynchronousSaving; enterWaitingLoop(); m_savingContext.synchronizingState = SavingContext::NormalSaving; } return true; } void EditorWindow::enterWaitingLoop() { //d->waitingLoop->exec(QEventLoop::ExcludeUserInputEvents); execSavingProgressDialog(); } void EditorWindow::quitWaitingLoop() { //d->waitingLoop->quit(); if (m_savingProgressDialog) { m_savingProgressDialog->close(); } } void EditorWindow::slotSelected(bool val) { // Update menu actions. d->cropAction->setEnabled(val); d->zoomFitToSelectAction->setEnabled(val); d->copyAction->setEnabled(val); QRect sel = m_canvas->getSelectedArea(); // Update histogram into sidebar. emit signalSelectionChanged(sel); // Update status bar if (val) { slotSelectionSetText(sel); } else { setToolInfoMessage(i18n("No selection")); } } void EditorWindow::slotPrepareToLoad() { // Disable actions as appropriate during loading emit signalNoCurrentItem(); unsetCursor(); m_animLogo->stop(); toggleActions(false); slotUpdateItemInfo(); } void EditorWindow::slotLoadingStarted(const QString& /*filename*/) { setCursor(Qt::WaitCursor); toggleActions(false); m_animLogo->start(); m_nameLabel->setProgressBarMode(StatusProgressBar::ProgressBarMode, i18n("Loading:")); } void EditorWindow::slotLoadingFinished(const QString& filename, bool success) { m_nameLabel->setProgressBarMode(StatusProgressBar::TextMode); // Enable actions as appropriate after loading // No need to re-enable image properties sidebar here, it's will be done // automatically by a signal from canvas toggleActions(success); slotUpdateItemInfo(); unsetCursor(); m_animLogo->stop(); if (success) { colorManage(); // Set a history which contains all available files as referredImages DImageHistory resolved = resolvedImageHistory(m_canvas->interface()->getInitialImageHistory()); m_canvas->interface()->setResolvedInitialHistory(resolved); } else { DNotificationPopup::message(DNotificationPopup::Boxed, i18n("Cannot load \"%1\"", filename), m_canvas, m_canvas->mapToGlobal(QPoint(30, 30))); } } void EditorWindow::resetOrigin() { // With versioning, "only" resetting undo history does not work anymore // as we calculate undo state based on the initial history stored in the DImg resetOriginSwitchFile(); } void EditorWindow::resetOriginSwitchFile() { DImageHistory resolved = resolvedImageHistory(m_canvas->interface()->getItemHistory()); m_canvas->interface()->switchToLastSaved(resolved); } void EditorWindow::colorManage() { if (!IccSettings::instance()->isEnabled()) { return; } DImg image = m_canvas->currentImage(); if (image.isNull()) { return; } if (!IccManager::needsPostLoadingManagement(image)) { return; } IccPostLoadingManager manager(image, m_canvas->currentImageFilePath()); if (!manager.hasValidWorkspace()) { QString message = i18n("Cannot open the specified working space profile (\"%1\"). " "No color transformation will be applied. " "Please check the color management " "configuration in digiKam's setup.", IccSettings::instance()->settings().workspaceProfile); QMessageBox::information(this, qApp->applicationName(), message); } // Show dialog and get transform from user choice IccTransform trans = manager.postLoadingManage(this); // apply transform in thread. // Do _not_ test for willHaveEffect() here - there are more side effects when calling this method m_canvas->applyTransform(trans); slotUpdateItemInfo(); } void EditorWindow::slotNameLabelCancelButtonPressed() { // If we saving an image... if (m_savingContext.savingState != SavingContext::SavingStateNone) { m_savingContext.abortingSaving = true; m_canvas->abortSaving(); } } void EditorWindow::slotFileOriginChanged(const QString&) { // implemented in subclass } bool EditorWindow::saveOrSaveAs() { if (m_canvas->isReadOnly()) { return saveAs(); } return save(); } void EditorWindow::slotSavingStarted(const QString& /*filename*/) { setCursor(Qt::WaitCursor); m_animLogo->start(); // Disable actions as appropriate during saving emit signalNoCurrentItem(); toggleActions(false); m_nameLabel->setProgressBarMode(StatusProgressBar::CancelProgressBarMode, i18n("Saving:")); } void EditorWindow::slotSavingFinished(const QString& filename, bool success) { Q_UNUSED(filename); qCDebug(DIGIKAM_GENERAL_LOG) << filename << success << (m_savingContext.savingState != SavingContext::SavingStateNone); // only handle this if we really wanted to save a file... if (m_savingContext.savingState != SavingContext::SavingStateNone) { m_savingContext.executedOperation = m_savingContext.savingState; m_savingContext.savingState = SavingContext::SavingStateNone; if (!success) { if (!m_savingContext.abortingSaving) { QMessageBox::critical(this, qApp->applicationName(), i18n("Failed to save file\n\"%1\"\nto\n\"%2\".", m_savingContext.destinationURL.fileName(), m_savingContext.destinationURL.toLocalFile())); } finishSaving(false); return; } moveFile(); } else { qCWarning(DIGIKAM_GENERAL_LOG) << "Why was slotSavingFinished called if we did not want to save a file?"; } } void EditorWindow::movingSaveFileFinished(bool successful) { if (!successful) { finishSaving(false); return; } // now that we know the real destination file name, pass it to be recorded in image history m_canvas->interface()->setLastSaved(m_savingContext.destinationURL.toLocalFile()); // remove image from cache since it has changed LoadingCacheInterface::fileChanged(m_savingContext.destinationURL.toLocalFile()); ThumbnailLoadThread::deleteThumbnail(m_savingContext.destinationURL.toLocalFile()); // restore state of disabled actions. saveIsComplete can start any other task // (loading!) which might itself in turn change states finishSaving(true); switch (m_savingContext.executedOperation) { case SavingContext::SavingStateNone: break; case SavingContext::SavingStateSave: saveIsComplete(); break; case SavingContext::SavingStateSaveAs: saveAsIsComplete(); break; case SavingContext::SavingStateVersion: saveVersionIsComplete(); break; } // Take all actions necessary to update information and re-enable sidebar slotChanged(); } void EditorWindow::finishSaving(bool success) { m_savingContext.synchronousSavingResult = success; delete m_savingContext.saveTempFile; m_savingContext.saveTempFile = nullptr; // Exit of internal Qt event loop to unlock promptUserSave() method. if (m_savingContext.synchronizingState == SavingContext::SynchronousSaving) { quitWaitingLoop(); } // Enable actions as appropriate after saving toggleActions(true); unsetCursor(); m_animLogo->stop(); m_nameLabel->setProgressBarMode(StatusProgressBar::TextMode); /*if (m_savingProgressDialog) { m_savingProgressDialog->close(); }*/ // On error, continue using current image if (!success) { /* Why this? * m_canvas->switchToLastSaved(m_savingContext.srcURL.toLocalFile());*/ } } void EditorWindow::setupTempSaveFile(const QUrl& url) { // if the destination url is on local file system, try to set the temp file // location to the destination folder, otherwise use a local default QString tempDir = url.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash).toLocalFile(); if (!url.isLocalFile() || tempDir.isEmpty()) { tempDir = QDir::tempPath(); } QFileInfo fi(url.toLocalFile()); QString suffix = fi.suffix(); // use magic file extension which tells the digikamalbums ioslave to ignore the file m_savingContext.saveTempFile = new SafeTemporaryFile(tempDir + QLatin1String("/EditorWindow-XXXXXX.digikamtempfile.") + suffix); m_savingContext.saveTempFile->setAutoRemove(false); if (!m_savingContext.saveTempFile->open()) { QMessageBox::critical(this, qApp->applicationName(), i18n("Could not open a temporary file in the folder \"%1\": %2 (%3)", QDir::toNativeSeparators(tempDir), m_savingContext.saveTempFile->errorString(), m_savingContext.saveTempFile->error())); return; } - m_savingContext.saveTempFileName = m_savingContext.saveTempFile->safeFileName(); + m_savingContext.saveTempFileName = m_savingContext.saveTempFile->safeFilePath(); delete m_savingContext.saveTempFile; m_savingContext.saveTempFile = nullptr; } void EditorWindow::startingSave(const QUrl& url) { qCDebug(DIGIKAM_GENERAL_LOG) << "startSaving url = " << url; // avoid any reentrancy. Should be impossible anyway since actions will be disabled. if (m_savingContext.savingState != SavingContext::SavingStateNone) { return; } m_savingContext = SavingContext(); if (!checkPermissions(url)) { return; } setupTempSaveFile(url); m_savingContext.srcURL = url; m_savingContext.destinationURL = m_savingContext.srcURL; m_savingContext.destinationExisted = true; m_savingContext.originalFormat = m_canvas->currentImageFileFormat(); m_savingContext.format = m_savingContext.originalFormat; m_savingContext.abortingSaving = false; m_savingContext.savingState = SavingContext::SavingStateSave; m_savingContext.executedOperation = SavingContext::SavingStateNone; m_canvas->interface()->saveAs(m_savingContext.saveTempFileName, m_IOFileSettings, m_setExifOrientationTag && m_canvas->exifRotated(), m_savingContext.format, m_savingContext.destinationURL.toLocalFile()); } bool EditorWindow::showFileSaveDialog(const QUrl& initialUrl, QUrl& newURL) { QString all; QStringList list = supportedImageMimeTypes(QIODevice::WriteOnly, all); DFileDialog* const imageFileSaveDialog = new DFileDialog(this); imageFileSaveDialog->setWindowTitle(i18n("New Image File Name")); imageFileSaveDialog->setDirectoryUrl(initialUrl.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash)); imageFileSaveDialog->setAcceptMode(QFileDialog::AcceptSave); imageFileSaveDialog->setFileMode(QFileDialog::AnyFile); imageFileSaveDialog->setNameFilters(list); // restore old settings for the dialog KSharedConfig::Ptr config = KSharedConfig::openConfig(); KConfigGroup group = config->group(configGroupName()); const QString optionLastExtension = QLatin1String("LastSavedImageExtension"); QString ext = group.readEntry(optionLastExtension, "png"); foreach (const QString& s, list) { if (s.contains(QString::fromLatin1("*.%1").arg(ext))) { imageFileSaveDialog->selectNameFilter(s); break; } } // adjust extension of proposed filename QString fileName = initialUrl.fileName(); if (!fileName.isNull()) { int lastDot = fileName.lastIndexOf(QLatin1Char('.')); QString completeBaseName = (lastDot == -1) ? fileName : fileName.left(lastDot); fileName = completeBaseName + QLatin1Char('.') + ext; } if (!fileName.isNull()) { imageFileSaveDialog->selectFile(fileName); } // Start dialog and check if canceled. int result; if (d->currentWindowModalDialog) { // go application-modal - we will create utter confusion if descending into more than one window-modal dialog imageFileSaveDialog->setModal(true); result = imageFileSaveDialog->exec(); } else { imageFileSaveDialog->setWindowModality(Qt::WindowModal); d->currentWindowModalDialog = imageFileSaveDialog; result = imageFileSaveDialog->exec(); d->currentWindowModalDialog = nullptr; } if (result != QDialog::Accepted || !imageFileSaveDialog) { qCDebug(DIGIKAM_GENERAL_LOG) << "File Save Dialog rejected"; return false; } QList urls = imageFileSaveDialog->selectedUrls(); if (urls.isEmpty()) { qCDebug(DIGIKAM_GENERAL_LOG) << "no target url"; return false; } newURL = urls.first(); newURL.setPath(QDir::cleanPath(newURL.path())); QFileInfo fi(newURL.fileName()); if (fi.suffix().isEmpty()) { ext = imageFileSaveDialog->selectedNameFilter().section(QLatin1String("*."), 1, 1); ext = ext.left(ext.length() - 1); if (ext.isEmpty()) { ext = QLatin1String("jpg"); } newURL.setPath(newURL.path() + QLatin1Char('.') + ext); } qCDebug(DIGIKAM_GENERAL_LOG) << "Writing file to " << newURL; //-- Show Settings Dialog ---------------------------------------------- const QString configShowImageSettingsDialog = QLatin1String("ShowImageSettingsDialog"); bool showDialog = group.readEntry(configShowImageSettingsDialog, true); FileSaveOptionsBox* const options = new FileSaveOptionsBox(); if (showDialog && options->discoverFormat(newURL.fileName(), DImg::NONE) != DImg::NONE) { FileSaveOptionsDlg* const fileSaveOptionsDialog = new FileSaveOptionsDlg(this, options); options->setImageFileFormat(newURL.fileName()); if (d->currentWindowModalDialog) { // go application-modal - we will create utter confusion if descending into more than one window-modal dialog fileSaveOptionsDialog->setModal(true); result = fileSaveOptionsDialog->exec(); } else { fileSaveOptionsDialog->setWindowModality(Qt::WindowModal); d->currentWindowModalDialog = fileSaveOptionsDialog; result = fileSaveOptionsDialog->exec(); d->currentWindowModalDialog = nullptr; } if (result != QDialog::Accepted || !fileSaveOptionsDialog) { return false; } } // write settings to config options->applySettings(); // read settings from config to local container applyIOSettings(); // select the format to save the image with m_savingContext.format = selectValidSavingFormat(newURL); if (m_savingContext.format.isNull()) { QMessageBox::critical(this, qApp->applicationName(), i18n("Unable to determine the format to save the target image with.")); return false; } if (!newURL.isValid()) { QMessageBox::critical(this, qApp->applicationName(), i18n("Cannot Save: Found file path %1 is invalid.", newURL.toDisplayString())); qCWarning(DIGIKAM_GENERAL_LOG) << "target URL is not valid !"; return false; } group.writeEntry(optionLastExtension, m_savingContext.format); config->sync(); return true; } QString EditorWindow::selectValidSavingFormat(const QUrl& targetUrl) { qCDebug(DIGIKAM_GENERAL_LOG) << "Trying to find a saving format from targetUrl = " << targetUrl; // build a list of valid types QString all; supportedImageMimeTypes(QIODevice::WriteOnly, all); qCDebug(DIGIKAM_GENERAL_LOG) << "Qt Offered types: " << all; QStringList validTypes = all.split(QLatin1String("*."), QString::SkipEmptyParts); validTypes.replaceInStrings(QLatin1String(" "), QString()); qCDebug(DIGIKAM_GENERAL_LOG) << "Writable formats: " << validTypes; // determine the format to use the format provided in the filename QString suffix; if (targetUrl.isLocalFile()) { // for local files QFileInfo can be used QFileInfo fi(targetUrl.toLocalFile()); suffix = fi.suffix(); qCDebug(DIGIKAM_GENERAL_LOG) << "Possible format from local file: " << suffix; } else { // for remote files string manipulation is needed unfortunately QString fileName = targetUrl.fileName(); const int periodLocation = fileName.lastIndexOf(QLatin1Char('.')); if (periodLocation >= 0) { suffix = fileName.right(fileName.size() - periodLocation - 1); } qCDebug(DIGIKAM_GENERAL_LOG) << "Possible format from remote file: " << suffix; } if (!suffix.isEmpty() && validTypes.contains(suffix, Qt::CaseInsensitive)) { qCDebug(DIGIKAM_GENERAL_LOG) << "Using format from target url " << suffix; return suffix; } // another way to determine the format is to use the original file { QString originalFormat = QString::fromUtf8(QImageReader::imageFormat(m_savingContext.srcURL.toLocalFile())); if (validTypes.contains(originalFormat, Qt::CaseInsensitive)) { qCDebug(DIGIKAM_GENERAL_LOG) << "Using format from original file: " << originalFormat; return originalFormat; } } qCDebug(DIGIKAM_GENERAL_LOG) << "No suitable format found"; return QString(); } bool EditorWindow::startingSaveAs(const QUrl& url) { qCDebug(DIGIKAM_GENERAL_LOG) << "startSavingAs called"; if (m_savingContext.savingState != SavingContext::SavingStateNone) { return false; } m_savingContext = SavingContext(); m_savingContext.srcURL = url; QUrl suggested = m_savingContext.srcURL; // Run dialog ------------------------------------------------------------------- QUrl newURL; if (!showFileSaveDialog(suggested, newURL)) { return false; } // if new and original URL are equal use save() ------------------------------ QUrl currURL(m_savingContext.srcURL); currURL.setPath(QDir::cleanPath(currURL.path())); newURL.setPath(QDir::cleanPath(newURL.path())); if (currURL.matches(newURL, QUrl::None)) { save(); return false; } // Check for overwrite ---------------------------------------------------------- QFileInfo fi(newURL.toLocalFile()); m_savingContext.destinationExisted = fi.exists(); if (m_savingContext.destinationExisted) { if (!checkOverwrite(newURL)) { return false; } // There will be two message boxes if the file is not writable. // This may be controversial, and it may be changed, but it was a deliberate decision. if (!checkPermissions(newURL)) { return false; } } // Now do the actual saving ----------------------------------------------------- setupTempSaveFile(newURL); m_savingContext.destinationURL = newURL; m_savingContext.originalFormat = m_canvas->currentImageFileFormat(); m_savingContext.savingState = SavingContext::SavingStateSaveAs; m_savingContext.executedOperation = SavingContext::SavingStateNone; m_savingContext.abortingSaving = false; // in any case, destructive (Save as) or non (Export), mark as New Version m_canvas->interface()->setHistoryIsBranch(true); m_canvas->interface()->saveAs(m_savingContext.saveTempFileName, m_IOFileSettings, m_setExifOrientationTag && m_canvas->exifRotated(), m_savingContext.format.toLower(), m_savingContext.destinationURL.toLocalFile()); return true; } bool EditorWindow::startingSaveCurrentVersion(const QUrl& url) { return startingSaveVersion(url, false, false, QString()); } bool EditorWindow::startingSaveNewVersion(const QUrl& url) { return startingSaveVersion(url, true, false, QString()); } bool EditorWindow::startingSaveNewVersionAs(const QUrl& url) { return startingSaveVersion(url, true, true, QString()); } bool EditorWindow::startingSaveNewVersionInFormat(const QUrl& url, const QString& format) { return startingSaveVersion(url, true, false, format); } VersionFileOperation EditorWindow::saveVersionFileOperation(const QUrl& url, bool fork) { DImageHistory resolvedHistory = m_canvas->interface()->getResolvedInitialHistory(); DImageHistory history = m_canvas->interface()->getItemHistory(); VersionFileInfo currentName(url.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash).toLocalFile(), url.fileName(), m_canvas->currentImageFileFormat()); return versionManager()->operation(fork ? VersionManager::NewVersionName : VersionManager::CurrentVersionName, currentName, resolvedHistory, history); } VersionFileOperation EditorWindow::saveAsVersionFileOperation(const QUrl& url, const QUrl& saveUrl, const QString& format) { DImageHistory resolvedHistory = m_canvas->interface()->getResolvedInitialHistory(); DImageHistory history = m_canvas->interface()->getItemHistory(); VersionFileInfo currentName(url.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash).toLocalFile(), url.fileName(), m_canvas->currentImageFileFormat()); VersionFileInfo saveLocation(saveUrl.adjusted(QUrl::RemoveFilename).toLocalFile(), saveUrl.fileName(), format); return versionManager()->operationNewVersionAs(currentName, saveLocation, resolvedHistory, history); } VersionFileOperation EditorWindow::saveInFormatVersionFileOperation(const QUrl& url, const QString& format) { DImageHistory resolvedHistory = m_canvas->interface()->getResolvedInitialHistory(); DImageHistory history = m_canvas->interface()->getItemHistory(); VersionFileInfo currentName(url.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash).toLocalFile(), url.fileName(), m_canvas->currentImageFileFormat()); return versionManager()->operationNewVersionInFormat(currentName, format, resolvedHistory, history); } bool EditorWindow::startingSaveVersion(const QUrl& url, bool fork, bool saveAs, const QString& format) { qCDebug(DIGIKAM_GENERAL_LOG) << "Saving image" << url << "non-destructive, new version:" << fork << ", saveAs:" << saveAs << "format:" << format; if (m_savingContext.savingState != SavingContext::SavingStateNone) { return false; } m_savingContext = SavingContext(); m_savingContext.versionFileOperation = saveVersionFileOperation(url, fork); m_canvas->interface()->setHistoryIsBranch(fork); if (saveAs) { QUrl suggested = m_savingContext.versionFileOperation.saveFile.fileUrl(); QUrl selectedUrl; if (!showFileSaveDialog(suggested, selectedUrl)) { return false; } m_savingContext.versionFileOperation = saveAsVersionFileOperation(url, selectedUrl, m_savingContext.format); } else if (!format.isNull()) { m_savingContext.versionFileOperation = saveInFormatVersionFileOperation(url, format); } const QUrl newURL = m_savingContext.versionFileOperation.saveFile.fileUrl(); qCDebug(DIGIKAM_GENERAL_LOG) << "Writing file to " << newURL; if (!newURL.isValid()) { QMessageBox::critical(this, qApp->applicationName(), i18nc("@info", "Cannot save file %1 to " "the suggested version file name %2", url.fileName(), newURL.fileName())); qCWarning(DIGIKAM_GENERAL_LOG) << "target URL is not valid !"; return false; } QFileInfo fi(newURL.toLocalFile()); m_savingContext.destinationExisted = fi.exists(); // Check for overwrite (saveAs only) -------------------------------------------- if (m_savingContext.destinationExisted) { // So, should we refuse to overwrite the original? // It's a frontal crash against non-destructive principles. // It is tempting to refuse, yet I think the user has to decide in the end /*QUrl currURL(m_savingContext.srcURL); currURL.cleanPath(); newURL.cleanPath(); if (currURL.equals(newURL)) { ... return false; }*/ // check for overwrite, unless the operation explicitly tells us to overwrite if (!(m_savingContext.versionFileOperation.tasks & VersionFileOperation::Replace) && !checkOverwrite(newURL)) { return false; } // There will be two message boxes if the file is not writable. // This may be controversial, and it may be changed, but it was a deliberate decision. if (!checkPermissions(newURL)) { return false; } } setupTempSaveFile(newURL); m_savingContext.srcURL = url; m_savingContext.destinationURL = newURL; m_savingContext.originalFormat = m_canvas->currentImageFileFormat(); m_savingContext.format = m_savingContext.versionFileOperation.saveFile.format; m_savingContext.abortingSaving = false; m_savingContext.savingState = SavingContext::SavingStateVersion; m_savingContext.executedOperation = SavingContext::SavingStateNone; m_canvas->interface()->saveAs(m_savingContext.saveTempFileName, m_IOFileSettings, m_setExifOrientationTag && m_canvas->exifRotated(), m_savingContext.format.toLower(), m_savingContext.versionFileOperation); return true; } bool EditorWindow::checkPermissions(const QUrl& url) { //TODO: Check that the permissions can actually be changed // if write permissions are not available. QFileInfo fi(url.toLocalFile()); if (fi.exists() && !fi.isWritable()) { int result = QMessageBox::warning(this, i18n("Overwrite File?"), i18n("You do not have write permissions " "for the file named \"%1\". " "Are you sure you want " "to overwrite it?", url.fileName()), QMessageBox::Save | QMessageBox::Cancel); if (result != QMessageBox::Save) { return false; } } return true; } bool EditorWindow::checkOverwrite(const QUrl& url) { int result = QMessageBox::warning(this, i18n("Overwrite File?"), i18n("A file named \"%1\" already " "exists. Are you sure you want " "to overwrite it?", url.fileName()), QMessageBox::Save | QMessageBox::Cancel); return (result == QMessageBox::Save); } bool EditorWindow::moveLocalFile(const QString& org, const QString& dst) { QString sidecarOrg = DMetadata::sidecarPath(org); QString source = m_savingContext.srcURL.toLocalFile(); if (QFileInfo::exists(sidecarOrg)) { QString sidecarDst = DMetadata::sidecarPath(dst); if (!DFileOperations::localFileRename(source, sidecarOrg, sidecarDst)) { qCDebug(DIGIKAM_GENERAL_LOG) << "Failed to move sidecar file"; } } if (!DFileOperations::localFileRename(source, org, dst)) { QMessageBox::critical(this, i18n("Error Saving File"), i18n("Failed to overwrite original file")); return false; } return true; } void EditorWindow::moveFile() { // Move local file. if (m_savingContext.executedOperation == SavingContext::SavingStateVersion) { // check if we need to move the current file to an intermediate name if (m_savingContext.versionFileOperation.tasks & VersionFileOperation::MoveToIntermediate) { //qCDebug(DIGIKAM_GENERAL_LOG) << "MoveToIntermediate: Moving " << m_savingContext.srcURL.toLocalFile() << "to" // << m_savingContext.versionFileOperation.intermediateForLoadedFile.filePath() moveLocalFile(m_savingContext.srcURL.toLocalFile(), m_savingContext.versionFileOperation.intermediateForLoadedFile.filePath()); LoadingCacheInterface::fileChanged(m_savingContext.destinationURL.toLocalFile()); ThumbnailLoadThread::deleteThumbnail(m_savingContext.destinationURL.toLocalFile()); } } bool moveSuccessful = moveLocalFile(m_savingContext.saveTempFileName, m_savingContext.destinationURL.toLocalFile()); if (m_savingContext.executedOperation == SavingContext::SavingStateVersion) { if (moveSuccessful && m_savingContext.versionFileOperation.tasks & VersionFileOperation::SaveAndDelete) { QFile file(m_savingContext.versionFileOperation.loadedFile.filePath()); file.remove(); } } movingSaveFileFinished(moveSuccessful); } void EditorWindow::slotDiscardChanges() { m_canvas->interface()->rollbackToOrigin(); } void EditorWindow::slotOpenOriginal() { // no-op in this base class } void EditorWindow::slotColorManagementOptionsChanged() { applyColorManagementSettings(); applyIOSettings(); } void EditorWindow::slotToggleColorManagedView() { if (!IccSettings::instance()->isEnabled()) { return; } bool cmv = !IccSettings::instance()->settings().useManagedView; IccSettings::instance()->setUseManagedView(cmv); } void EditorWindow::setColorManagedViewIndicatorToolTip(bool available, bool cmv) { QString tooltip; if (available) { if (cmv) { tooltip = i18n("Color-Managed View is enabled."); } else { tooltip = i18n("Color-Managed View is disabled."); } } else { tooltip = i18n("Color Management is not configured, so the Color-Managed View is not available."); } d->cmViewIndicator->setToolTip(tooltip); } void EditorWindow::slotSoftProofingOptions() { // Adjusts global settings QPointer dlg = new SoftProofDialog(this); dlg->exec(); d->viewSoftProofAction->setChecked(dlg->shallEnableSoftProofView()); slotUpdateSoftProofingState(); delete dlg; } void EditorWindow::slotUpdateSoftProofingState() { bool on = d->viewSoftProofAction->isChecked(); m_canvas->setSoftProofingEnabled(on); d->toolIface->updateICCSettings(); } void EditorWindow::slotSetUnderExposureIndicator(bool on) { d->exposureSettings->underExposureIndicator = on; d->toolIface->updateExposureSettings(); d->viewUnderExpoAction->setChecked(on); setUnderExposureToolTip(on); } void EditorWindow::setUnderExposureToolTip(bool on) { d->underExposureIndicator->setToolTip( on ? i18n("Under-Exposure indicator is enabled") : i18n("Under-Exposure indicator is disabled")); } void EditorWindow::slotSetOverExposureIndicator(bool on) { d->exposureSettings->overExposureIndicator = on; d->toolIface->updateExposureSettings(); d->viewOverExpoAction->setChecked(on); setOverExposureToolTip(on); } void EditorWindow::setOverExposureToolTip(bool on) { d->overExposureIndicator->setToolTip( on ? i18n("Over-Exposure indicator is enabled") : i18n("Over-Exposure indicator is disabled")); } void EditorWindow::slotSelectionChanged(const QRect& sel) { slotSelectionSetText(sel); emit signalSelectionChanged(sel); } void EditorWindow::slotSelectionSetText(const QRect& sel) { setToolInfoMessage(QString::fromLatin1("(%1, %2) (%3 x %4)").arg(sel.x()).arg(sel.y()).arg(sel.width()).arg(sel.height())); } void EditorWindow::slotComponentsInfo() { LibsInfoDlg* const dlg = new LibsInfoDlg(this); dlg->show(); } void EditorWindow::setToolStartProgress(const QString& toolName) { m_animLogo->start(); m_nameLabel->setProgressValue(0); m_nameLabel->setProgressBarMode(StatusProgressBar::CancelProgressBarMode, QString::fromUtf8("%1:").arg(toolName)); } void EditorWindow::setToolProgress(int progress) { m_nameLabel->setProgressValue(progress); } void EditorWindow::setToolStopProgress() { m_animLogo->stop(); m_nameLabel->setProgressValue(0); m_nameLabel->setProgressBarMode(StatusProgressBar::TextMode); slotUpdateItemInfo(); } void EditorWindow::slotCloseTool() { if (d->toolIface) { d->toolIface->slotCloseTool(); } } void EditorWindow::slotApplyTool() { if (d->toolIface) { d->toolIface->slotApplyTool(); } } void EditorWindow::setPreviewModeMask(int mask) { d->previewToolBar->setPreviewModeMask(mask); } PreviewToolBar::PreviewMode EditorWindow::previewMode() const { return d->previewToolBar->previewMode(); } void EditorWindow::setToolInfoMessage(const QString& txt) { d->infoLabel->setAdjustedText(txt); } VersionManager* EditorWindow::versionManager() const { return &d->defaultVersionManager; } void EditorWindow::setupSelectToolsAction() { // Create action model ActionItemModel* const actionModel = new ActionItemModel(this); actionModel->setMode(ActionItemModel::ToplevelMenuCategory | ActionItemModel::SortCategoriesByInsertionOrder); // Builtin actions QString transformCategory = i18nc("@title Image Transform", "Transform"); foreach (DPluginAction* const ac, DPluginLoader::instance()->pluginsActions(DPluginAction::EditorTransform, this)) { actionModel->addAction(ac, transformCategory); } QString decorateCategory = i18nc("@title Image Decorate", "Decorate"); foreach (DPluginAction* const ac, DPluginLoader::instance()->pluginsActions(DPluginAction::EditorDecorate, this)) { actionModel->addAction(ac, decorateCategory); } QString effectsCategory = i18nc("@title Image Effect", "Effects"); foreach (DPluginAction* const ac, DPluginLoader::instance()->pluginsActions(DPluginAction::EditorFilters, this)) { actionModel->addAction(ac, effectsCategory); } QString colorsCategory = i18nc("@title Image Colors", "Colors"); foreach (DPluginAction* const ac, DPluginLoader::instance()->pluginsActions(DPluginAction::EditorColors, this)) { actionModel->addAction(ac, effectsCategory); } QString enhanceCategory = i18nc("@title Image Enhance", "Enhance"); foreach (DPluginAction* const ac, DPluginLoader::instance()->pluginsActions(DPluginAction::EditorEnhance, this)) { actionModel->addAction(ac, effectsCategory); } QString postCategory = i18nc("@title Post Processing Tools", "Post-Processing"); foreach (DPluginAction* const ac, DPluginLoader::instance()->pluginsActions(DPluginAction::GenericTool, this)) { actionModel->addAction(ac, postCategory); } foreach (DPluginAction* const ac, DPluginLoader::instance()->pluginsActions(DPluginAction::EditorFile, this)) { actionModel->addAction(ac, postCategory); } foreach (DPluginAction* const ac, DPluginLoader::instance()->pluginsActions(DPluginAction::GenericMetadata, this)) { actionModel->addAction(ac, postCategory); } QString exportCategory = i18nc("@title Export Tools", "Export"); foreach (DPluginAction* const ac, DPluginLoader::instance()->pluginsActions(DPluginAction::GenericExport, this)) { actionModel->addAction(ac, exportCategory); } QString importCategory = i18nc("@title Import Tools", "Import"); foreach (DPluginAction* const ac, DPluginLoader::instance()->pluginsActions(DPluginAction::GenericImport, this)) { actionModel->addAction(ac, importCategory); } // setup categorized view DCategorizedSortFilterProxyModel* const filterModel = actionModel->createFilterModel(); ActionCategorizedView* const selectToolsActionView = new ActionCategorizedView; selectToolsActionView->setIconSize(QSize(22, 22)); selectToolsActionView->setModel(filterModel); selectToolsActionView->setupIconMode(); selectToolsActionView->adjustGridSize(); connect(selectToolsActionView, SIGNAL(clicked(QModelIndex)), actionModel, SLOT(trigger(QModelIndex))); EditorToolIface::editorToolIface()->setToolsIconView(selectToolsActionView); } void EditorWindow::slotThemeChanged() { KSharedConfig::Ptr config = KSharedConfig::openConfig(); KConfigGroup group = config->group(configGroupName()); if (!group.readEntry(d->configUseThemeBackgroundColorEntry, true)) { m_bgColor = group.readEntry(d->configBackgroundColorEntry, QColor(Qt::black)); } else { m_bgColor = palette().color(QPalette::Base); } m_canvas->setBackgroundBrush(QBrush(m_bgColor)); d->toolIface->themeChanged(); } void EditorWindow::addAction2ContextMenu(const QString& actionName, bool addDisabled) { if (!m_contextMenu) { return; } QAction* const action = actionCollection()->action(actionName); if (action && (action->isEnabled() || addDisabled)) { m_contextMenu->addAction(action); } } void EditorWindow::showSideBars(bool visible) { if (visible) { rightSideBar()->restore(QList() << thumbBar(), d->fullscreenSizeBackup); } else { // See bug #166472, a simple backup()/restore() will hide non-sidebar splitter child widgets // in horizontal mode thumbbar wont be member of the splitter, it is just ignored then rightSideBar()->backup(QList() << thumbBar(), &d->fullscreenSizeBackup); } } void EditorWindow::slotToggleRightSideBar() { rightSideBar()->isExpanded() ? rightSideBar()->shrink() : rightSideBar()->expand(); } void EditorWindow::slotPreviousRightSideBarTab() { rightSideBar()->activePreviousTab(); } void EditorWindow::slotNextRightSideBarTab() { rightSideBar()->activeNextTab(); } void EditorWindow::showThumbBar(bool visible) { visible ? thumbBar()->restoreVisibility() : thumbBar()->hide(); } bool EditorWindow::thumbbarVisibility() const { return thumbBar()->isVisible(); } void EditorWindow::customizedFullScreenMode(bool set) { set ? m_canvas->setBackgroundBrush(QBrush(Qt::black)) : m_canvas->setBackgroundBrush(QBrush(m_bgColor)); showStatusBarAction()->setEnabled(!set); toolBarMenuAction()->setEnabled(!set); showMenuBarAction()->setEnabled(!set); m_showBarAction->setEnabled(!set); } void EditorWindow::addServicesMenuForUrl(const QUrl& url) { KService::List offers = DServiceMenu::servicesForOpenWith(QList() << url); qCDebug(DIGIKAM_GENERAL_LOG) << offers.count() << " services found to open " << url; if (m_servicesMenu) { delete m_servicesMenu; m_servicesMenu = nullptr; } if (m_serviceAction) { delete m_serviceAction; m_serviceAction = nullptr; } if (!offers.isEmpty()) { m_servicesMenu = new QMenu(this); QAction* const serviceAction = m_servicesMenu->menuAction(); serviceAction->setText(i18n("Open With")); foreach (const KService::Ptr& service, offers) { QString name = service->name().replace(QLatin1Char('&'), QLatin1String("&&")); QAction* const action = m_servicesMenu->addAction(name); action->setIcon(QIcon::fromTheme(service->icon())); action->setData(service->name()); d->servicesMap[name] = service; } #ifdef HAVE_KIO m_servicesMenu->addSeparator(); m_servicesMenu->addAction(i18n("Other...")); m_contextMenu->addAction(serviceAction); connect(m_servicesMenu, SIGNAL(triggered(QAction*)), this, SLOT(slotOpenWith(QAction*))); } else { m_serviceAction = new QAction(i18n("Open With..."), this); m_contextMenu->addAction(m_serviceAction); connect(m_servicesMenu, SIGNAL(triggered()), this, SLOT(slotOpenWith())); #endif // HAVE_KIO } } void EditorWindow::openWith(const QUrl& url, QAction* action) { KService::Ptr service; QString name = action ? action->data().toString() : QString(); #ifdef HAVE_KIO if (name.isEmpty()) { QPointer dlg = new KOpenWithDialog(QList() << url); if (dlg->exec() != KOpenWithDialog::Accepted) { delete dlg; return; } service = dlg->service(); if (!service) { // User entered a custom command if (!dlg->text().isEmpty()) { DServiceMenu::runFiles(dlg->text(), QList() << url); } delete dlg; return; } delete dlg; } else #endif // HAVE_KIO { service = d->servicesMap[name]; } DServiceMenu::runFiles(service.data(), QList() << url); } void EditorWindow::loadTool(EditorTool* const tool) { EditorToolIface::editorToolIface()->loadTool(tool); connect(tool, SIGNAL(okClicked()), this, SLOT(slotToolDone())); connect(tool, SIGNAL(cancelClicked()), this, SLOT(slotToolDone())); } void EditorWindow::slotToolDone() { EditorToolIface::editorToolIface()->unLoadTool(); } void EditorWindow::slotRotateLeftIntoQue() { m_transformQue.append(TransformType::RotateLeft); } void EditorWindow::slotRotateRightIntoQue() { m_transformQue.append(TransformType::RotateRight); } void EditorWindow::slotFlipHIntoQue() { m_transformQue.append(TransformType::FlipHorizontal); } void EditorWindow::slotFlipVIntoQue() { m_transformQue.append(TransformType::FlipVertical); } void EditorWindow::registerExtraPluginsActions(QString& dom) { DPluginLoader* const dpl = DPluginLoader::instance(); dpl->registerEditorPlugins(this); dpl->registerRawImportPlugins(this); QList actions = dpl->pluginsActions(DPluginAction::Editor, this); foreach (DPluginAction* const ac, actions) { actionCollection()->addActions(QList() << ac); actionCollection()->setDefaultShortcuts(ac, ac->shortcuts()); } dom.replace(QLatin1String(""), dpl->pluginXmlSections(DPluginAction::EditorFile, this)); dom.replace(QLatin1String(""), dpl->pluginXmlSections(DPluginAction::EditorColors, this)); dom.replace(QLatin1String(""), dpl->pluginXmlSections(DPluginAction::EditorEnhance, this)); QString transformActions; transformActions.append(QLatin1String("\n")); transformActions.append(QLatin1String("\n")); transformActions.append(QLatin1String("\n")); transformActions.append(QLatin1String("\n")); transformActions.append(QLatin1String("\n")); transformActions.append(QLatin1String("\n")); transformActions.append(dpl->pluginXmlSections(DPluginAction::EditorTransform, this)); dom.replace(QLatin1String(""), transformActions); dom.replace(QLatin1String(""), dpl->pluginXmlSections(DPluginAction::EditorDecorate, this)); dom.replace(QLatin1String(""), dpl->pluginXmlSections(DPluginAction::EditorFilters, this)); } } // namespace Digikam diff --git a/core/utilities/queuemanager/manager/batchtool.cpp b/core/utilities/queuemanager/manager/batchtool.cpp index 1dcd435a7f..12e9930692 100644 --- a/core/utilities/queuemanager/manager/batchtool.cpp +++ b/core/utilities/queuemanager/manager/batchtool.cpp @@ -1,638 +1,638 @@ /* ============================================================ * * This file is a part of digiKam project * https://www.digikam.org * * Date : 2008-11-24 * Description : Batch Tool Container. * * Copyright (C) 2008-2020 by Gilles Caulier * * 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. * * ============================================================ */ #include "batchtool.h" // Qt includes #include #include #include #include #include #include #include // KDE includes #include // Local includes #include "drawdecoder.h" #include "digikam_debug.h" #include "dimgbuiltinfilter.h" #include "dimgloaderobserver.h" #include "dimgthreadedfilter.h" #include "filereadwritelock.h" #include "batchtoolutils.h" #include "jpegsettings.h" #include "pngsettings.h" #include "dmetadata.h" #include "dpluginbqm.h" namespace Digikam { class BatchToolObserver; class Q_DECL_HIDDEN BatchTool::Private { public: explicit Private() : exifResetOrientation(false), exifCanEditOrientation(true), saveAsNewVersion(true), branchHistory(true), cancel(false), last(false), observer(nullptr), toolGroup(BaseTool), rawLoadingRule(QueueSettings::DEMOSAICING), plugin(nullptr) { } bool exifResetOrientation; bool exifCanEditOrientation; bool saveAsNewVersion; bool branchHistory; bool cancel; bool last; QString errorMessage; QString toolTitle; ///< User friendly tool title. QString toolDescription; ///< User friendly tool description. QIcon toolIcon; QUrl inputUrl; QUrl outputUrl; QUrl workingUrl; DImg image; ItemInfo imageinfo; DRawDecoderSettings rawDecodingSettings; IOFileSettings ioFileSettings; BatchToolSettings settings; BatchToolObserver* observer; BatchTool::BatchToolGroup toolGroup; QueueSettings::RawLoadingRule rawLoadingRule; DPluginBqm* plugin; }; class Q_DECL_HIDDEN BatchToolObserver : public DImgLoaderObserver { public: explicit BatchToolObserver(BatchTool::Private* const priv) : DImgLoaderObserver(), d(priv) { } ~BatchToolObserver() { } bool continueQuery() override { return (!d->cancel); } private: BatchTool::Private* const d; }; BatchTool::BatchTool(const QString& name, BatchToolGroup group, QObject* const parent) : QObject(parent), d(new Private) { d->observer = new BatchToolObserver(d); d->toolGroup = group; m_settingsWidget = nullptr; setObjectName(name); } BatchTool::~BatchTool() { // NOTE: See bug #341566: no need to delete settings widget here. // We delete the settings widget now in the ToolSettingsView. delete d->observer; delete d; } void BatchTool::setPlugin(DPluginBqm* const plugin) { d->plugin = plugin; setToolTitle(d->plugin->name()); setToolDescription(d->plugin->description()); setToolIcon(d->plugin->icon()); connect(d->plugin, SIGNAL(signalVisible(bool)), this, SIGNAL(signalVisible(bool))); } DPluginBqm* BatchTool::plugin() const { return d->plugin; } QString BatchTool::errorDescription() const { return d->errorMessage; } void BatchTool::setErrorDescription(const QString& errmsg) { d->errorMessage = errmsg; } BatchTool::BatchToolGroup BatchTool::toolGroup() const { return d->toolGroup; } QString BatchTool::toolGroupToString() const { switch (toolGroup()) { case BaseTool: return i18n("Base"); case CustomTool: return i18n("Custom"); case ColorTool: return i18n("Colors"); case EnhanceTool: return i18n("Enhance"); case TransformTool: return i18n("Transform"); case DecorateTool: return i18n("Decorate"); case FiltersTool: return i18n("Filters"); case ConvertTool: return i18n("Convert"); case MetadataTool: return i18n("Metadata"); default: break; } return i18n("Invalid"); } void BatchTool::setToolTitle(const QString& toolTitle) { d->toolTitle = toolTitle; } QString BatchTool::toolTitle() const { return d->toolTitle; } void BatchTool::setToolDescription(const QString& toolDescription) { d->toolDescription = toolDescription; } QString BatchTool::toolDescription() const { return d->toolDescription; } void BatchTool::setToolIconName(const QString& iconName) { d->toolIcon = QIcon::fromTheme(iconName); } void BatchTool::setToolIcon(const QIcon& icon) { d->toolIcon = icon; } QIcon BatchTool::toolIcon() const { return d->toolIcon; } void BatchTool::slotResetSettingsToDefault() { slotSettingsChanged(defaultSettings()); } void BatchTool::slotSettingsChanged(const BatchToolSettings& settings) { setSettings(settings); emit signalSettingsChanged(d->settings); } void BatchTool::setSettings(const BatchToolSettings& settings) { d->settings = settings; emit signalAssignSettings2Widget(); } void BatchTool::setInputUrl(const QUrl& inputUrl) { d->inputUrl = inputUrl; } QUrl BatchTool::inputUrl() const { return d->inputUrl; } void BatchTool::setOutputUrl(const QUrl& outputUrl) { d->outputUrl = outputUrl; } QUrl BatchTool::outputUrl() const { return d->outputUrl; } QString BatchTool::outputSuffix() const { return QString(); } void BatchTool::setImageData(const DImg& img) { d->image = img; } DImg BatchTool::imageData() const { return d->image; } void BatchTool::setItemInfo(const ItemInfo& info) { d->imageinfo = info; } ItemInfo BatchTool::imageInfo() const { return d->imageinfo; } void BatchTool::setNeedResetExifOrientation(bool set) { d->exifResetOrientation = set; } bool BatchTool::getNeedResetExifOrientation() const { return d->exifResetOrientation; } void BatchTool::setResetExifOrientationAllowed(bool set) { d->exifCanEditOrientation = set; } bool BatchTool::getResetExifOrientationAllowed() const { return d->exifCanEditOrientation; } void BatchTool::setRawLoadingRules(QueueSettings::RawLoadingRule rule) { d->rawLoadingRule = rule; } void BatchTool::setSaveAsNewVersion(bool fork) { d->saveAsNewVersion = fork; } void BatchTool::setBranchHistory(bool branch) { d->branchHistory = branch; } bool BatchTool::getBranchHistory() const { return d->branchHistory; } void BatchTool::setDRawDecoderSettings(const DRawDecoderSettings& settings) { d->rawDecodingSettings = settings; } DRawDecoderSettings BatchTool::rawDecodingSettings() const { return d->rawDecodingSettings; } void BatchTool::setIOFileSettings(const IOFileSettings& settings) { d->ioFileSettings = settings; } IOFileSettings BatchTool::ioFileSettings() const { return d->ioFileSettings; } void BatchTool::setWorkingUrl(const QUrl& workingUrl) { d->workingUrl = workingUrl; } QUrl BatchTool::workingUrl() const { return d->workingUrl; } void BatchTool::cancel() { d->cancel = true; } bool BatchTool::isCancelled() const { return d->cancel; } BatchToolSettings BatchTool::settings() const { return d->settings; } void BatchTool::setLastChainedTool(bool last) { d->last = last; } bool BatchTool::isLastChainedTool() const { return d->last; } void BatchTool::setOutputUrlFromInputUrl() { QString randomString(QUuid::createUuid().toString()); QString path(workingUrl().toLocalFile()); QString suffix = outputSuffix(); if (suffix.isEmpty()) { QFileInfo fi(inputUrl().fileName()); suffix = fi.suffix(); } SafeTemporaryFile temp(path + QLatin1String("/BatchTool-XXXXXX-") + randomString.mid(1, 8) + QLatin1String(".digikamtempfile.") + suffix); temp.setAutoRemove(false); temp.open(); - qCDebug(DIGIKAM_GENERAL_LOG) << "path: " << temp.safeFileName(); + qCDebug(DIGIKAM_GENERAL_LOG) << "path: " << temp.safeFilePath(); - setOutputUrl(QUrl::fromLocalFile(temp.safeFileName())); + setOutputUrl(QUrl::fromLocalFile(temp.safeFilePath())); } bool BatchTool::isRawFile(const QUrl& url) const { QString rawFilesExt = DRawDecoder::rawFiles(); QFileInfo fileInfo(url.toLocalFile()); return (rawFilesExt.toUpper().contains(fileInfo.suffix().toUpper())); } bool BatchTool::loadToDImg() const { if (!d->image.isNull()) { return true; } if ((d->rawLoadingRule == QueueSettings::USEEMBEDEDJPEG) && isRawFile(inputUrl())) { QImage img; bool ret = DRawDecoder::loadRawPreview(img, inputUrl().toLocalFile()); DMetadata meta(inputUrl().toLocalFile()); meta.setItemDimensions(QSize(img.width(), img.height())); d->image = DImg(img); d->image.setMetadata(meta.data()); return ret; } return (d->image.load(inputUrl().toLocalFile(), d->observer, DRawDecoding(rawDecodingSettings()))); } bool BatchTool::savefromDImg() const { if (!isLastChainedTool() && outputSuffix().isEmpty()) { return true; } DImg::FORMAT detectedFormat = d->image.detectedFormat(); QString frm = outputSuffix().toUpper(); bool resetOrientation = getResetExifOrientationAllowed() && (getNeedResetExifOrientation() || (detectedFormat == DImg::RAW)); if (d->branchHistory) { // image has its original history d->image.setHistoryBranch(d->saveAsNewVersion); } if (frm.isEmpty()) { // In case of output support is not set for ex. with all tool which do not convert to new format. if (detectedFormat == DImg::JPEG) { d->image.setAttribute(QLatin1String("quality"), JPEGSettings::convertCompressionForLibJpeg(ioFileSettings().JPEGCompression)); d->image.setAttribute(QLatin1String("subsampling"), ioFileSettings().JPEGSubSampling); } else if (detectedFormat == DImg::PNG) { d->image.setAttribute(QLatin1String("quality"), PNGSettings::convertCompressionForLibPng(ioFileSettings().PNGCompression)); } else if (detectedFormat == DImg::TIFF) { d->image.setAttribute(QLatin1String("compress"), ioFileSettings().TIFFCompression); } else if (detectedFormat == DImg::JP2K) { d->image.setAttribute(QLatin1String("quality"), ioFileSettings().JPEG2000LossLess ? 100 : ioFileSettings().JPEG2000Compression); } else if (detectedFormat == DImg::PGF) { d->image.setAttribute(QLatin1String("quality"), ioFileSettings().PGFLossLess ? 0 : ioFileSettings().PGFCompression); } d->image.prepareMetadataToSave(outputUrl().toLocalFile(), DImg::formatToMimeType(detectedFormat), resetOrientation); bool b = d->image.save(outputUrl().toLocalFile(), detectedFormat, d->observer); return b; } d->image.prepareMetadataToSave(outputUrl().toLocalFile(), frm, resetOrientation); bool b = d->image.save(outputUrl().toLocalFile(), frm, d->observer); d->image = DImg(); return b; } DImg& BatchTool::image() const { return d->image; } bool BatchTool::apply() { d->cancel = false; qCDebug(DIGIKAM_GENERAL_LOG) << "Tool: " << toolTitle(); qCDebug(DIGIKAM_GENERAL_LOG) << "Input url: " << inputUrl(); qCDebug(DIGIKAM_GENERAL_LOG) << "Output url: " << outputUrl(); //qCDebug(DIGIKAM_GENERAL_LOG) << "Settings: "; BatchToolSettings prm = settings(); for (BatchToolSettings::const_iterator it = prm.constBegin() ; it != prm.constEnd() ; ++it) { if (it.value().canConvert()) { QPolygon pol = it.value().value(); int size = (pol.size() > 20) ? 20 : pol.size(); QString tmp; tmp.append(QString::fromUtf8("[%1 items] : ").arg(pol.size())); for (int i = 0 ; i < size ; ++i) { tmp.append(QLatin1String("(")); tmp.append(QString::number(pol.point(i).x())); tmp.append(QLatin1String(", ")); tmp.append(QString::number(pol.point(i).y())); tmp.append(QLatin1String(") ")); } //qCDebug(DIGIKAM_GENERAL_LOG) << " " << it.key() << ": " << tmp; } else { //qCDebug(DIGIKAM_GENERAL_LOG) << " " << it.key() << ": " << it.value(); } } return toolOperations(); } void BatchTool::applyFilter(DImgThreadedFilter* const filter) { filter->startFilterDirectly(); if (isCancelled()) { return; } d->image.putImageData(filter->getTargetImage().bits()); d->image.addFilterAction(filter->filterAction()); } void BatchTool::applyFilterChangedProperties(DImgThreadedFilter* const filter) { filter->startFilterDirectly(); if (isCancelled()) { return; } DImg trg = filter->getTargetImage(); d->image.putImageData(trg.width(), trg.height(), trg.sixteenBit(), trg.hasAlpha(), trg.bits()); d->image.addFilterAction(filter->filterAction()); } void BatchTool::applyFilter(DImgBuiltinFilter* const filter) { filter->apply(d->image); d->image.addFilterAction(filter->filterAction()); } // -- Settings Widgets methods --------------------------------------------------------------------------- QWidget* BatchTool::settingsWidget() const { return m_settingsWidget; } void BatchTool::deleteSettingsWidget() { delete m_settingsWidget; m_settingsWidget = nullptr; } void BatchTool::registerSettingsWidget() { // NOTE: see bug #209225 : signal/slot connection used internally to prevent crash when settings // are assigned to settings widget by main thread to tool thread. connect(this, SIGNAL(signalAssignSettings2Widget()), this, SLOT(slotAssignSettings2Widget())); if (!m_settingsWidget) { QLabel* const label = new QLabel; label->setText(i18n("No setting available")); label->setAlignment(Qt::AlignCenter); label->setWordWrap(true); m_settingsWidget = label; } } } // namespace Digikam