diff --git a/app/generalconfigpage.ui b/app/generalconfigpage.ui index d874d8a5..343ee43b 100644 --- a/app/generalconfigpage.ui +++ b/app/generalconfigpage.ui @@ -1,157 +1,193 @@ GeneralConfigPage 0 0 470 338 Videos: Show videos - + Qt::Vertical QSizePolicy::Fixed 20 20 Background color: 256 0 255 Qt::Horizontal 0 0 40 0 true QFrame::StyledPanel QFrame::Sunken Qt::Horizontal 40 20 Qt::Vertical QSizePolicy::Fixed 20 20 + + + JPEG save quality: + + + + + + + 1 + + + 100 + + + % + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 20 + + + + + Thumbnail actions: - + All buttons - + Show selection button only - + None diff --git a/app/gvcore.cpp b/app/gvcore.cpp index a57670a2..2b15a020 100644 --- a/app/gvcore.cpp +++ b/app/gvcore.cpp @@ -1,474 +1,532 @@ // vim: set tabstop=4 shiftwidth=4 expandtab: /* Gwenview: an image viewer Copyright 2008 Aurélien Gâteau 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 of the License, 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. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Cambridge, MA 02110-1301, USA. */ // Self #include "gvcore.h" #include "dialogguard.h" // Qt #include #include +#include #include +#include #include #include +#include +#include // KDE -#include #include +#include +#include #include #include // Local #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace Gwenview { struct GvCorePrivate { GvCore* q; MainWindow* mMainWindow; SortedDirModel* mDirModel; HistoryModel* mRecentFoldersModel; RecentFilesModel* mRecentFilesModel; QPalette mPalettes[4]; QString mFullScreenPaletteName; + int configFileJPEGQualityValue; bool showSaveAsDialog(const QUrl &url, QUrl* outUrl, QByteArray* format) { - DialogGuard dialog(mMainWindow); - dialog->setAcceptMode(QFileDialog::AcceptSave); + // Build the JPEG quality chooser custom widget + QWidget* JPEGQualityChooserWidget = new QWidget; + JPEGQualityChooserWidget->setVisible(false); // shown only for JPEGs + + QLabel* JPEGQualityChooserLabel = new QLabel; + JPEGQualityChooserLabel->setText(i18n("JPEG quality:")); + JPEGQualityChooserLabel->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); + + QSpinBox* JPEGQualityChooserSpinBox = new QSpinBox; + JPEGQualityChooserSpinBox->setMinimum(1); + JPEGQualityChooserSpinBox->setMaximum(100); + JPEGQualityChooserSpinBox->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); + JPEGQualityChooserSpinBox->setSuffix(i18nc("Spinbox suffix; percentage 1 - 100", "%")); + configFileJPEGQualityValue = GwenviewConfig::jPEGQuality(); + JPEGQualityChooserSpinBox->setValue(configFileJPEGQualityValue); + + // Temporarily change JPEG quality value + QObject::connect(JPEGQualityChooserSpinBox, QOverload::of(&QSpinBox::valueChanged), + JPEGQualityChooserSpinBox, [=](int value) { + GwenviewConfig::setJPEGQuality(value); + }); + + QSpacerItem* horizontalSpacer = new QSpacerItem(1, 1, QSizePolicy::Expanding, QSizePolicy::Fixed); + + QHBoxLayout* JPEGQualityChooserLayout = new QHBoxLayout(JPEGQualityChooserWidget); + JPEGQualityChooserLayout->setContentsMargins(0,0,0,0); + JPEGQualityChooserLayout->addWidget(JPEGQualityChooserLabel); + JPEGQualityChooserLayout->addWidget(JPEGQualityChooserSpinBox); + JPEGQualityChooserLayout->addItem(horizontalSpacer); + + // Set up the dialog + DialogGuard dialog(mMainWindow); + KFileWidget* fileWidget = dialog->fileWidget(); + dialog->setCustomWidget(JPEGQualityChooserWidget); + dialog->setOperationMode(KFileWidget::Saving); dialog->setWindowTitle(i18nc("@title:window", "Save Image")); // Temporary workaround for selectUrl() not setting the // initial directory to url (removed in D4193) - dialog->setDirectoryUrl(url.adjusted(QUrl::RemoveFilename)); - dialog->selectUrl(url); + dialog->setUrl(url.adjusted(QUrl::RemoveFilename)); + fileWidget->setSelectedUrl(url); QStringList supportedMimetypes; for (const QByteArray &mimeName : QImageWriter::supportedMimeTypes()) { supportedMimetypes.append(QString::fromLocal8Bit(mimeName)); } - dialog->setMimeTypeFilters(supportedMimetypes); - dialog->selectMimeTypeFilter(MimeTypeUtils::urlMimeType(url)); + fileWidget->setMimeFilter(supportedMimetypes, + MimeTypeUtils::urlMimeType(url)); + + // Only show the JPEG quality chooser when saving a JPEG image + QObject::connect(fileWidget, &KFileWidget::filterChanged, + JPEGQualityChooserWidget, [=](const QString &filter) { + JPEGQualityChooserWidget->setVisible(filter.contains(QStringLiteral("jpeg"))); + }); // Show dialog do { if (!dialog->exec()) { return false; } - QList files = dialog->selectedUrls(); + QList files = fileWidget->selectedUrls(); if (files.isEmpty()) { return false; } QString filename = files.first().fileName(); const QMimeType mimeType = QMimeDatabase().mimeTypeForFile(filename, QMimeDatabase::MatchExtension); if (mimeType.isValid()) { *format = mimeType.preferredSuffix().toLocal8Bit(); break; } KMessageBox::sorry( mMainWindow, i18nc("@info", "Gwenview cannot save images as %1.", QFileInfo(filename).suffix()) ); } while (true); - *outUrl = dialog->selectedUrls().first(); + *outUrl = fileWidget->selectedUrls().first(); return true; } void setupPalettes() { QPalette pal; int value = GwenviewConfig::viewBackgroundValue(); QColor fgColor = value > 128 ? Qt::black : Qt::white; // Normal KSharedConfigPtr config = KSharedConfig::openConfig(); mPalettes[GvCore::NormalPalette] = KColorScheme::createApplicationPalette(config); pal = mPalettes[GvCore::NormalPalette]; pal.setColor(QPalette::Base, QColor::fromHsv(0, 0, value)); pal.setColor(QPalette::Text, fgColor); mPalettes[GvCore::NormalViewPalette] = pal; // Fullscreen QString name = GwenviewConfig::fullScreenColorScheme(); if (name.isEmpty()) { // Default color scheme mFullScreenPaletteName = QStandardPaths::locate(QStandardPaths::AppDataLocation, "color-schemes/fullscreen.colors"); config = KSharedConfig::openConfig(mFullScreenPaletteName); } else if (name.contains('/')) { // Full path to a .colors file mFullScreenPaletteName = name; config = KSharedConfig::openConfig(mFullScreenPaletteName); } else { // Standard KDE color scheme mFullScreenPaletteName = QStringLiteral("color-schemes/%1.colors").arg(name); config = KSharedConfig::openConfig(mFullScreenPaletteName, KConfig::FullConfig, QStandardPaths::AppDataLocation); } mPalettes[GvCore::FullScreenPalette] = KColorScheme::createApplicationPalette(config); // If we are using the default palette, adjust it to match the system color scheme if (name.isEmpty()) { adjustDefaultFullScreenPalette(); } // FullScreenView has textured background pal = mPalettes[GvCore::FullScreenPalette]; QString path = QStandardPaths::locate(QStandardPaths::AppDataLocation, "images/background.png"); QPixmap bgTexture(path); pal.setBrush(QPalette::Base, bgTexture); mPalettes[GvCore::FullScreenViewPalette] = pal; } void adjustDefaultFullScreenPalette() { // The Fullscreen palette by default does not use the system color scheme, and therefore uses an 'accent' color // of blue. So for every color group/role combination that uses the accent color, we use a muted version of the // Normal palette. We also use the normal HighlightedText color so it properly contrasts with Highlight. const QPalette normalPal = mPalettes[GvCore::NormalPalette]; QPalette fullscreenPal = mPalettes[GvCore::FullScreenPalette]; // Colors from the normal palette (source of the system theme's accent color) const QColor normalToolTipBase = normalPal.color(QPalette::Normal, QPalette::ToolTipBase); const QColor normalToolTipText = normalPal.color(QPalette::Normal, QPalette::ToolTipText); const QColor normalHighlight = normalPal.color(QPalette::Normal, QPalette::Highlight); const QColor normalHighlightedText = normalPal.color(QPalette::Normal, QPalette::HighlightedText); const QColor normalLink = normalPal.color(QPalette::Normal, QPalette::Link); const QColor normalActiveToolTipBase = normalPal.color(QPalette::Active, QPalette::ToolTipBase); const QColor normalActiveToolTipText = normalPal.color(QPalette::Active, QPalette::ToolTipText); const QColor normalActiveHighlight = normalPal.color(QPalette::Active, QPalette::Highlight); const QColor normalActiveHighlightedText = normalPal.color(QPalette::Active, QPalette::HighlightedText); const QColor normalActiveLink = normalPal.color(QPalette::Active, QPalette::Link); const QColor normalDisabledToolTipBase = normalPal.color(QPalette::Disabled, QPalette::ToolTipBase); const QColor normalDisabledToolTipText = normalPal.color(QPalette::Disabled, QPalette::ToolTipText); // Note: Disabled Highlight missing as they do not use the accent color const QColor normalDisabledLink = normalPal.color(QPalette::Disabled, QPalette::Link); const QColor normalInactiveToolTipBase = normalPal.color(QPalette::Inactive, QPalette::ToolTipBase); const QColor normalInactiveToolTipText = normalPal.color(QPalette::Inactive, QPalette::ToolTipText); const QColor normalInactiveHighlight = normalPal.color(QPalette::Inactive, QPalette::Highlight); const QColor normalInactiveHighlightedText = normalPal.color(QPalette::Inactive, QPalette::HighlightedText); const QColor normalInactiveLink = normalPal.color(QPalette::Inactive, QPalette::Link); // Colors of the fullscreen palette which we will be modifying QColor fullScreenToolTipBase = fullscreenPal.color(QPalette::Normal, QPalette::ToolTipBase); QColor fullScreenToolTipText = fullscreenPal.color(QPalette::Normal, QPalette::ToolTipText); QColor fullScreenHighlight = fullscreenPal.color(QPalette::Normal, QPalette::Highlight); QColor fullScreenLink = fullscreenPal.color(QPalette::Normal, QPalette::Link); QColor fullScreenActiveToolTipBase = fullscreenPal.color(QPalette::Active, QPalette::ToolTipBase); QColor fullScreenActiveToolTipText = fullscreenPal.color(QPalette::Active, QPalette::ToolTipText); QColor fullScreenActiveHighlight = fullscreenPal.color(QPalette::Active, QPalette::Highlight); QColor fullScreenActiveLink = fullscreenPal.color(QPalette::Active, QPalette::Link); QColor fullScreenDisabledToolTipBase = fullscreenPal.color(QPalette::Disabled, QPalette::ToolTipBase); QColor fullScreenDisabledToolTipText = fullscreenPal.color(QPalette::Disabled, QPalette::ToolTipText); QColor fullScreenDisabledLink = fullscreenPal.color(QPalette::Disabled, QPalette::Link); QColor fullScreenInactiveToolTipBase = fullscreenPal.color(QPalette::Inactive, QPalette::ToolTipBase); QColor fullScreenInactiveToolTipText = fullscreenPal.color(QPalette::Inactive, QPalette::ToolTipText); QColor fullScreenInactiveHighlight = fullscreenPal.color(QPalette::Inactive, QPalette::Highlight); QColor fullScreenInactiveLink = fullscreenPal.color(QPalette::Inactive, QPalette::Link); // Adjust the value of the normal color so it's not too dark/bright, and apply to the respective fullscreen color fullScreenToolTipBase .setHsv(normalToolTipBase.hue(), normalToolTipBase.saturation(), (127 + 2 * normalToolTipBase.value()) / 3); fullScreenToolTipText .setHsv(normalToolTipText.hue(), normalToolTipText.saturation(), (127 + 2 * normalToolTipText.value()) / 3); fullScreenHighlight .setHsv(normalHighlight.hue(), normalHighlight.saturation(), (127 + 2 * normalHighlight.value()) / 3); fullScreenLink .setHsv(normalLink.hue(), normalLink.saturation(), (127 + 2 * normalLink.value()) / 3); fullScreenActiveToolTipBase .setHsv(normalActiveToolTipBase.hue(), normalActiveToolTipBase.saturation(), (127 + 2 * normalActiveToolTipBase.value()) / 3); fullScreenActiveToolTipText .setHsv(normalActiveToolTipText.hue(), normalActiveToolTipText.saturation(), (127 + 2 * normalActiveToolTipText.value()) / 3); fullScreenActiveHighlight .setHsv(normalActiveHighlight.hue(), normalActiveHighlight.saturation(), (127 + 2 * normalActiveHighlight.value()) / 3); fullScreenActiveLink .setHsv(normalActiveLink.hue(), normalActiveLink.saturation(), (127 + 2 * normalActiveLink.value()) / 3); fullScreenDisabledToolTipBase.setHsv(normalDisabledToolTipBase.hue(), normalDisabledToolTipBase.saturation(), (127 + 2 * normalDisabledToolTipBase.value()) / 3); fullScreenDisabledToolTipText.setHsv(normalDisabledToolTipText.hue(), normalDisabledToolTipText.saturation(), (127 + 2 * normalDisabledToolTipText.value()) / 3); fullScreenDisabledLink .setHsv(normalDisabledLink.hue(), normalDisabledLink.saturation(), (127 + 2 * normalDisabledLink.value()) / 3); fullScreenInactiveToolTipBase.setHsv(normalInactiveToolTipBase.hue(), normalInactiveToolTipBase.saturation(), (127 + 2 * normalInactiveToolTipBase.value()) / 3); fullScreenInactiveToolTipText.setHsv(normalInactiveToolTipText.hue(), normalInactiveToolTipText.saturation(), (127 + 2 * normalInactiveToolTipText.value()) / 3); fullScreenInactiveHighlight .setHsv(normalInactiveHighlight.hue(), normalInactiveHighlight.saturation(), (127 + 2 * normalInactiveHighlight.value()) / 3); fullScreenInactiveLink .setHsv(normalInactiveLink.hue(), normalInactiveLink.saturation(), (127 + 2 * normalInactiveLink.value()) / 3); // Apply the modified colors to the fullscreen palette fullscreenPal.setColor(QPalette::Normal, QPalette::ToolTipBase, fullScreenToolTipBase); fullscreenPal.setColor(QPalette::Normal, QPalette::ToolTipText, fullScreenToolTipText); fullscreenPal.setColor(QPalette::Normal, QPalette::Highlight, fullScreenHighlight); fullscreenPal.setColor(QPalette::Normal, QPalette::Link, fullScreenLink); fullscreenPal.setColor(QPalette::Active, QPalette::ToolTipBase, fullScreenActiveToolTipBase); fullscreenPal.setColor(QPalette::Active, QPalette::ToolTipText, fullScreenActiveToolTipText); fullscreenPal.setColor(QPalette::Active, QPalette::Highlight, fullScreenActiveHighlight); fullscreenPal.setColor(QPalette::Active, QPalette::Link, fullScreenActiveLink); fullscreenPal.setColor(QPalette::Disabled, QPalette::ToolTipBase, fullScreenDisabledToolTipBase); fullscreenPal.setColor(QPalette::Disabled, QPalette::ToolTipText, fullScreenDisabledToolTipText); fullscreenPal.setColor(QPalette::Disabled, QPalette::Link, fullScreenDisabledLink); fullscreenPal.setColor(QPalette::Inactive, QPalette::ToolTipBase, fullScreenInactiveToolTipBase); fullscreenPal.setColor(QPalette::Inactive, QPalette::ToolTipText, fullScreenInactiveToolTipText); fullscreenPal.setColor(QPalette::Inactive, QPalette::Highlight, fullScreenInactiveHighlight); fullscreenPal.setColor(QPalette::Inactive, QPalette::Link, fullScreenInactiveLink); // Since we use an adjusted version of the normal highlight color, we need to use the normal version of the // text color so it contrasts fullscreenPal.setColor(QPalette::Normal, QPalette::HighlightedText, normalHighlightedText); fullscreenPal.setColor(QPalette::Active, QPalette::HighlightedText, normalActiveHighlightedText); fullscreenPal.setColor(QPalette::Inactive, QPalette::HighlightedText, normalInactiveHighlightedText); mPalettes[GvCore::FullScreenPalette] = fullscreenPal; } }; GvCore::GvCore(MainWindow* mainWindow, SortedDirModel* dirModel) : QObject(mainWindow) , d(new GvCorePrivate) { d->q = this; d->mMainWindow = mainWindow; d->mDirModel = dirModel; d->mRecentFoldersModel = nullptr; d->mRecentFilesModel = nullptr; d->setupPalettes(); connect(GwenviewConfig::self(), SIGNAL(configChanged()), SLOT(slotConfigChanged())); } GvCore::~GvCore() { delete d; } QAbstractItemModel* GvCore::recentFoldersModel() const { if (!d->mRecentFoldersModel) { d->mRecentFoldersModel = new HistoryModel(const_cast(this), QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation) + "/recentfolders/"); } return d->mRecentFoldersModel; } QAbstractItemModel* GvCore::recentFilesModel() const { if (!d->mRecentFilesModel) { d->mRecentFilesModel = new RecentFilesModel(const_cast(this)); } return d->mRecentFilesModel; } AbstractSemanticInfoBackEnd* GvCore::semanticInfoBackEnd() const { return d->mDirModel->semanticInfoBackEnd(); } SortedDirModel* GvCore::sortedDirModel() const { return d->mDirModel; } void GvCore::addUrlToRecentFolders(QUrl url) { if (!GwenviewConfig::historyEnabled()) { return; } if (!url.isValid()) { return; } // For "sftp://localhost", "/" is a different path than "" (bug #312060) if (!url.path().isEmpty() && !url.path().endsWith('/')) { url.setPath(url.path() + '/'); } recentFoldersModel(); d->mRecentFoldersModel->addUrl(url); } void GvCore::addUrlToRecentFiles(const QUrl &url) { if (!GwenviewConfig::historyEnabled()) { return; } recentFilesModel(); d->mRecentFilesModel->addUrl(url); } void GvCore::saveAll() { SaveAllHelper helper(d->mMainWindow); helper.save(); } void GvCore::save(const QUrl &url) { Document::Ptr doc = DocumentFactory::instance()->load(url); QByteArray format = doc->format(); const QByteArrayList availableTypes = QImageWriter::supportedImageFormats(); if (availableTypes.contains(format)) { DocumentJob* job = doc->save(url, format); connect(job, SIGNAL(result(KJob*)), SLOT(slotSaveResult(KJob*))); } else { // We don't know how to save in 'format', ask the user for a format we can // write to. KGuiItem saveUsingAnotherFormat = KStandardGuiItem::saveAs(); saveUsingAnotherFormat.setText(i18n("Save using another format")); int result = KMessageBox::warningContinueCancel( d->mMainWindow, i18n("Gwenview cannot save images in '%1' format.", QString(format)), QString() /* caption */, saveUsingAnotherFormat ); if (result == KMessageBox::Continue) { saveAs(url); } } } void GvCore::saveAs(const QUrl &url) { QByteArray format; QUrl saveAsUrl; if (!d->showSaveAsDialog(url, &saveAsUrl, &format)) { return; } + if (format == "jpg") { + // Gwenview code assumes JPEG images have "jpeg" format, so if the + // dialog returned the format "jpg", use "jpeg" instead + // This does not affect the actual filename extension + format = "jpeg"; + } + // Start save Document::Ptr doc = DocumentFactory::instance()->load(url); - KJob* job = doc->save(saveAsUrl, format.data()); + KJob* job = doc->save(saveAsUrl, format); if (!job) { const QString name = saveAsUrl.fileName().isEmpty() ? saveAsUrl.toDisplayString() : saveAsUrl.fileName(); const QString msg = xi18nc("@info", "Saving %1 failed:%2", name, doc->errorString()); KMessageBox::sorry(QApplication::activeWindow(), msg); } else { connect(job, SIGNAL(result(KJob*)), SLOT(slotSaveResult(KJob*))); } } static void applyTransform(const QUrl &url, Orientation orientation) { TransformImageOperation* op = new TransformImageOperation(orientation); Document::Ptr doc = DocumentFactory::instance()->load(url); op->applyToDocument(doc); } void GvCore::slotSaveResult(KJob* _job) { + // Regardless of job result, reset JPEG config value if it was changed by + // the Save As dialog + if (GwenviewConfig::jPEGQuality() != d->configFileJPEGQualityValue) { + GwenviewConfig::setJPEGQuality(d->configFileJPEGQualityValue); + } + SaveJob* job = static_cast(_job); QUrl oldUrl = job->oldUrl(); QUrl newUrl = job->newUrl(); if (job->error()) { QString name = newUrl.fileName().isEmpty() ? newUrl.toDisplayString() : newUrl.fileName(); const QString msg = xi18nc("@info", "Saving %1 failed:%2", name, kxi18n(qPrintable(job->errorString()))); int result = KMessageBox::warningContinueCancel( d->mMainWindow, msg, QString() /* caption */, KStandardGuiItem::saveAs()); if (result == KMessageBox::Continue) { saveAs(oldUrl); } return; } if (oldUrl != newUrl) { d->mMainWindow->goToUrl(newUrl); ViewMainPage* page = d->mMainWindow->viewMainPage(); if (page->isVisible()) { HudMessageBubble* bubble = new HudMessageBubble(); bubble->setText(i18n("You are now viewing the new document.")); KGuiItem item = KStandardGuiItem::back(); item.setText(i18n("Go back to the original")); HudButton* button = bubble->addButton(item); BinderRef::bind(button, SIGNAL(clicked()), d->mMainWindow, &MainWindow::goToUrl, oldUrl); connect(button, SIGNAL(clicked()), bubble, SLOT(deleteLater())); page->showMessageWidget(bubble); } } } void GvCore::rotateLeft(const QUrl &url) { applyTransform(url, ROT_270); } void GvCore::rotateRight(const QUrl &url) { applyTransform(url, ROT_90); } void GvCore::setRating(const QUrl &url, int rating) { QModelIndex index = d->mDirModel->indexForUrl(url); if (!index.isValid()) { qWarning() << "invalid index!"; return; } d->mDirModel->setData(index, rating, SemanticInfoDirModel::RatingRole); } static void clearModel(QAbstractItemModel* model) { model->removeRows(0, model->rowCount()); } void GvCore::clearRecentFilesAndFolders() { clearModel(recentFilesModel()); clearModel(recentFoldersModel()); } void GvCore::slotConfigChanged() { if (!GwenviewConfig::historyEnabled()) { clearRecentFilesAndFolders(); } d->setupPalettes(); } QPalette GvCore::palette(GvCore::PaletteType type) const { return d->mPalettes[type]; } QString GvCore::fullScreenPaletteName() const { return d->mFullScreenPaletteName; } } // namespace diff --git a/lib/document/documentloadedimpl.cpp b/lib/document/documentloadedimpl.cpp index 6e0026a2..126e7908 100644 --- a/lib/document/documentloadedimpl.cpp +++ b/lib/document/documentloadedimpl.cpp @@ -1,123 +1,128 @@ // vim: set tabstop=4 shiftwidth=4 expandtab: /* Gwenview: an image viewer Copyright 2007 Aurélien Gâteau 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 of the License, 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. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ // Self #include "documentloadedimpl.h" // Qt #include #include #include #include #include #include // KDE // Local #include "documentjob.h" +#include "gwenviewconfig.h" #include "imageutils.h" #include "savejob.h" namespace Gwenview { struct DocumentLoadedImplPrivate { QByteArray mRawData; bool mQuietInit; }; DocumentLoadedImpl::DocumentLoadedImpl(Document* document, const QByteArray& rawData, bool quietInit) : AbstractDocumentImpl(document) , d(new DocumentLoadedImplPrivate) { if (document->keepRawData()) { d->mRawData = rawData; } d->mQuietInit = quietInit; } DocumentLoadedImpl::~DocumentLoadedImpl() { delete d; } void DocumentLoadedImpl::init() { if (!d->mQuietInit) { emit imageRectUpdated(document()->image().rect()); emit loaded(); } } bool DocumentLoadedImpl::isEditable() const { return true; } Document::LoadingState DocumentLoadedImpl::loadingState() const { return Document::Loaded; } bool DocumentLoadedImpl::saveInternal(QIODevice* device, const QByteArray& format) { QImageWriter writer(device, format); + // If we're saving a non-JPEG image as a JPEG, respect the quality setting + if (format == QStringLiteral("jpeg")) { + writer.setQuality(GwenviewConfig::jPEGQuality()); + } bool ok = writer.write(document()->image()); if (ok) { setDocumentFormat(format); } else { setDocumentErrorString(writer.errorString()); } return ok; } DocumentJob* DocumentLoadedImpl::save(const QUrl &url, const QByteArray& format) { return new SaveJob(this, url, format); } AbstractDocumentEditor* DocumentLoadedImpl::editor() { return this; } void DocumentLoadedImpl::setImage(const QImage& image) { setDocumentImage(image); emit imageRectUpdated(image.rect()); } void DocumentLoadedImpl::applyTransformation(Orientation orientation) { QImage image = document()->image(); QTransform matrix = ImageUtils::transformMatrix(orientation); image = image.transformed(matrix); setDocumentImage(image); emit imageRectUpdated(image.rect()); } QByteArray DocumentLoadedImpl::rawData() const { return d->mRawData; } } // namespace diff --git a/lib/gwenviewconfig.kcfg b/lib/gwenviewconfig.kcfg index 140aec7e..bf640240 100644 --- a/lib/gwenviewconfig.kcfg +++ b/lib/gwenviewconfig.kcfg @@ -1,342 +1,346 @@ lib/sorting.h lib/zoommode.h lib/thumbnailactions.h lib/mousewheelbehavior.h lib/documentview/documentview.h lib/documentview/rasterimageview.h lib/print/printoptionspage.h lib/renderingintent.h General.Name,General.ImageSize,Exif.Photo.ExposureTime,Exif.Photo.Flash true true false 100 true 0.5 The percentage of memory used by Gwenview before it warns the user and suggest saving changes. new A list of filename extensions Gwenview should not try to load. We exclude *.new as well because this is the extension used for temporary files by KSaveFile. false + + 90 + + Horizontal 1 false false information ThumbnailActions::AllButtons General.Name,Exif.Image.DateTime true false AbstractImageView::AlphaBackgroundNone #ffffff MouseWheelBehavior::Scroll false true 350, 100 DocumentView::SoftwareAnimation ZoomMode::Autofit Defines what happens when going to image B after having zoomed in on an area of image A. If set to Autofit, image B is zoomed out to fit the screen. If set to KeepSame, all images share the same zoom and position: image B is set to the same zoom parameters as image A (and if these are changed, image A will then be displayed with the updated zoom and position). If set to Individual, all images remember their own zoom and position: image B is initially set to the same zoom parameters as image A, but will then remember its own zoom and position (if these are changed, image A will NOT be displayed with the updated zoom and position). RenderingIntent::Perceptual Defines how colors are rendered when your display uses an ICC color profile and an image has colors that do not fit within the profile's color gamut. "Perceptual" will scale the colors of the entire image so that they all fit within the display's capabilities. "Relative" will squash only the colors that cannot be displayed, and leave the other colors alone. 128 3./2. false Sorting::Name false 1 true Qt::AlignHCenter | Qt::AlignVCenter PrintOptionsPage::ScaleToPage false 15.0 10.0 PrintOptionsPage::Centimeters true false true false false 5.0 24 false false -1 0 0 true true false diff --git a/lib/jpegcontent.cpp b/lib/jpegcontent.cpp index 6ac9c1cb..772d01cc 100644 --- a/lib/jpegcontent.cpp +++ b/lib/jpegcontent.cpp @@ -1,713 +1,714 @@ // vim: set tabstop=4 shiftwidth=4 expandtab: /* Gwenview: an image viewer Copyright 2007 Aurélien Gâteau 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 of the License, 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. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "jpegcontent.h" // System #include #include #include #include extern "C" { #include #include "transupp.h" } // Qt #include #include #include #include #include #include // KDE #include // Exiv2 #include // Local #include "jpegerrormanager.h" #include "iodevicejpegsourcemanager.h" #include "exiv2imageloader.h" #include "gwenviewconfig.h" #include "imageutils.h" namespace Gwenview { const int INMEM_DST_DELTA = 4096; //----------------------------------------------- // // In-memory data destination manager for libjpeg // //----------------------------------------------- struct inmem_dest_mgr : public jpeg_destination_mgr { QByteArray* mOutput; void dump() { qDebug() << "dest_mgr:\n"; qDebug() << "- next_output_byte: " << next_output_byte; qDebug() << "- free_in_buffer: " << free_in_buffer; qDebug() << "- output size: " << mOutput->size(); } }; void inmem_init_destination(j_compress_ptr cinfo) { inmem_dest_mgr* dest = (inmem_dest_mgr*)(cinfo->dest); if (dest->mOutput->size() == 0) { dest->mOutput->resize(INMEM_DST_DELTA); } dest->free_in_buffer = dest->mOutput->size(); dest->next_output_byte = (JOCTET*)(dest->mOutput->data()); } boolean inmem_empty_output_buffer(j_compress_ptr cinfo) { inmem_dest_mgr* dest = (inmem_dest_mgr*)(cinfo->dest); dest->mOutput->resize(dest->mOutput->size() + INMEM_DST_DELTA); dest->next_output_byte = (JOCTET*)(dest->mOutput->data() + dest->mOutput->size() - INMEM_DST_DELTA); dest->free_in_buffer = INMEM_DST_DELTA; return true; } void inmem_term_destination(j_compress_ptr cinfo) { inmem_dest_mgr* dest = (inmem_dest_mgr*)(cinfo->dest); int finalSize = dest->next_output_byte - (JOCTET*)(dest->mOutput->data()); Q_ASSERT(finalSize >= 0); dest->mOutput->resize(finalSize); } //--------------------- // // JpegContent::Private // //--------------------- struct JpegContent::Private { // JpegContent usually stores the image pixels as compressed JPEG data in // mRawData. However if the image is set with setImage() because the user // performed a lossy image manipulation, mRawData is cleared and the image // pixels are kept in mImage until updateRawDataFromImage() is called. QImage mImage; // Store the input file, keep it open readOnly. This allows the file to be memory mapped // (i.e. mRawData may point to mFile.map()) rather than completely read on load. Postpone // QFile::readAll() as long as possible (currently in save()). QFile mFile; QByteArray mRawData; QSize mSize; QString mComment; bool mPendingTransformation; QTransform mTransformMatrix; Exiv2::ExifData mExifData; QString mErrorString; Private() { mPendingTransformation = false; } void setupInmemDestination(j_compress_ptr cinfo, QByteArray* outputData) { Q_ASSERT(!cinfo->dest); inmem_dest_mgr* dest = (inmem_dest_mgr*) (*cinfo->mem->alloc_small)((j_common_ptr) cinfo, JPOOL_PERMANENT, sizeof(inmem_dest_mgr)); cinfo->dest = (struct jpeg_destination_mgr*)(dest); dest->init_destination = inmem_init_destination; dest->empty_output_buffer = inmem_empty_output_buffer; dest->term_destination = inmem_term_destination; dest->mOutput = outputData; } bool readSize() { struct jpeg_decompress_struct srcinfo; // Init JPEG structs JPEGErrorManager errorManager; // Initialize the JPEG decompression object srcinfo.err = &errorManager; jpeg_create_decompress(&srcinfo); if (setjmp(errorManager.jmp_buffer)) { qCritical() << "libjpeg fatal error\n"; return false; } // Specify data source for decompression QBuffer buffer(&mRawData); buffer.open(QIODevice::ReadOnly); IODeviceJpegSourceManager::setup(&srcinfo, &buffer); // Read the header jcopy_markers_setup(&srcinfo, JCOPYOPT_ALL); int result = jpeg_read_header(&srcinfo, true); if (result != JPEG_HEADER_OK) { qCritical() << "Could not read jpeg header\n"; jpeg_destroy_decompress(&srcinfo); return false; } mSize = QSize(srcinfo.image_width, srcinfo.image_height); jpeg_destroy_decompress(&srcinfo); return true; } bool updateRawDataFromImage() { QBuffer buffer; QImageWriter writer(&buffer, "jpeg"); + writer.setQuality(GwenviewConfig::jPEGQuality()); if (!writer.write(mImage)) { mErrorString = writer.errorString(); return false; } mRawData = buffer.data(); mImage = QImage(); return true; } }; //------------ // // JpegContent // //------------ JpegContent::JpegContent() { d = new JpegContent::Private(); } JpegContent::~JpegContent() { delete d; } bool JpegContent::load(const QString& path) { if (d->mFile.isOpen()) { d->mFile.unmap(reinterpret_cast(d->mRawData.data())); d->mFile.close(); d->mRawData.clear(); } d->mFile.setFileName(path); if (!d->mFile.open(QIODevice::ReadOnly)) { qCritical() << "Could not open '" << path << "' for reading\n"; return false; } QByteArray rawData; uchar* mappedFile = d->mFile.map(0, d->mFile.size(), QFileDevice::MapPrivateOption); if (mappedFile == nullptr) { // process' mapping limit exceeded, file is sealed or filesystem doesn't support it, etc. qDebug() << "Could not mmap '" << path << "', falling back to QFile::readAll()\n"; rawData = d->mFile.readAll(); // all read in, no need to keep it open d->mFile.close(); } else { rawData = QByteArray::fromRawData(reinterpret_cast(mappedFile), d->mFile.size()); } return loadFromData(rawData); } bool JpegContent::loadFromData(const QByteArray& data) { std::unique_ptr image; Exiv2ImageLoader loader; if (!loader.load(data)) { qCritical() << "Could not load image with Exiv2, reported error:" << loader.errorMessage(); } image.reset(loader.popImage().release()); return loadFromData(data, image.get()); } bool JpegContent::loadFromData(const QByteArray& data, Exiv2::Image* exiv2Image) { d->mPendingTransformation = false; d->mTransformMatrix.reset(); d->mRawData = data; if (d->mRawData.size() == 0) { qCritical() << "No data\n"; return false; } if (!d->readSize()) return false; d->mExifData = exiv2Image->exifData(); d->mComment = QString::fromUtf8(exiv2Image->comment().c_str()); if (!GwenviewConfig::applyExifOrientation()) { return true; } // Adjust the size according to the orientation switch (orientation()) { case TRANSPOSE: case ROT_90: case TRANSVERSE: case ROT_270: d->mSize.transpose(); break; default: break; } return true; } QByteArray JpegContent::rawData() const { return d->mRawData; } Orientation JpegContent::orientation() const { Exiv2::ExifKey key("Exif.Image.Orientation"); Exiv2::ExifData::iterator it = d->mExifData.findKey(key); // We do the same checks as in libexiv2's src/crwimage.cpp: // http://dev.exiv2.org/projects/exiv2/repository/entry/trunk/src/crwimage.cpp?rev=2681#L1336 if (it == d->mExifData.end() || it->count() == 0 || it->typeId() != Exiv2::unsignedShort) { return NOT_AVAILABLE; } return Orientation(it->toLong()); } int JpegContent::dotsPerMeterX() const { return dotsPerMeter(QStringLiteral("XResolution")); } int JpegContent::dotsPerMeterY() const { return dotsPerMeter(QStringLiteral("YResolution")); } int JpegContent::dotsPerMeter(const QString& keyName) const { Exiv2::ExifKey keyResUnit("Exif.Image.ResolutionUnit"); Exiv2::ExifData::iterator it = d->mExifData.findKey(keyResUnit); if (it == d->mExifData.end()) { return 0; } int res = it->toLong(); QString keyVal = QStringLiteral("Exif.Image.") + keyName; Exiv2::ExifKey keyResolution(keyVal.toLocal8Bit().data()); it = d->mExifData.findKey(keyResolution); if (it == d->mExifData.end()) { return 0; } // The unit for measuring XResolution and YResolution. The same unit is used for both XResolution and YResolution. // If the image resolution in unknown, 2 (inches) is designated. // Default = 2 // 2 = inches // 3 = centimeters // Other = reserved const float INCHESPERMETER = (100. / 2.54); switch (res) { case 3: // dots per cm return int(it->toLong() * 100); default: // dots per inch return int(it->toLong() * INCHESPERMETER); } return 0; } void JpegContent::resetOrientation() { Exiv2::ExifKey key("Exif.Image.Orientation"); Exiv2::ExifData::iterator it = d->mExifData.findKey(key); if (it == d->mExifData.end()) { return; } *it = uint16_t(NORMAL); } QSize JpegContent::size() const { return d->mSize; } QString JpegContent::comment() const { return d->mComment; } void JpegContent::setComment(const QString& comment) { d->mComment = comment; } static QTransform createRotMatrix(int angle) { QTransform matrix; matrix.rotate(angle); return matrix; } static QTransform createScaleMatrix(int dx, int dy) { QTransform matrix; matrix.scale(dx, dy); return matrix; } struct OrientationInfo { OrientationInfo() : orientation(NOT_AVAILABLE) , jxform(JXFORM_NONE) {} OrientationInfo(Orientation o, const QTransform &m, JXFORM_CODE j) : orientation(o), matrix(m), jxform(j) {} Orientation orientation; QTransform matrix; JXFORM_CODE jxform; }; typedef QList OrientationInfoList; static const OrientationInfoList& orientationInfoList() { static OrientationInfoList list; if (list.size() == 0) { QTransform rot90 = createRotMatrix(90); QTransform hflip = createScaleMatrix(-1, 1); QTransform vflip = createScaleMatrix(1, -1); list << OrientationInfo() << OrientationInfo(NORMAL, QTransform(), JXFORM_NONE) << OrientationInfo(HFLIP, hflip, JXFORM_FLIP_H) << OrientationInfo(ROT_180, createRotMatrix(180), JXFORM_ROT_180) << OrientationInfo(VFLIP, vflip, JXFORM_FLIP_V) << OrientationInfo(TRANSPOSE, hflip * rot90, JXFORM_TRANSPOSE) << OrientationInfo(ROT_90, rot90, JXFORM_ROT_90) << OrientationInfo(TRANSVERSE, vflip * rot90, JXFORM_TRANSVERSE) << OrientationInfo(ROT_270, createRotMatrix(270), JXFORM_ROT_270) ; } return list; } void JpegContent::transform(Orientation orientation) { if (orientation != NOT_AVAILABLE && orientation != NORMAL) { d->mPendingTransformation = true; OrientationInfoList::ConstIterator it(orientationInfoList().begin()), end(orientationInfoList().end()); for (; it != end; ++it) { if ((*it).orientation == orientation) { d->mTransformMatrix = (*it).matrix * d->mTransformMatrix; break; } } if (it == end) { qWarning() << "Could not find matrix for orientation\n"; } } } #if 0 static void dumpMatrix(const QTransform& matrix) { qDebug() << "matrix | " << matrix.m11() << ", " << matrix.m12() << " |\n"; qDebug() << " | " << matrix.m21() << ", " << matrix.m22() << " |\n"; qDebug() << " ( " << matrix.dx() << ", " << matrix.dy() << " )\n"; } #endif static bool matricesAreSame(const QTransform& m1, const QTransform& m2, double tolerance) { return fabs(m1.m11() - m2.m11()) < tolerance && fabs(m1.m12() - m2.m12()) < tolerance && fabs(m1.m21() - m2.m21()) < tolerance && fabs(m1.m22() - m2.m22()) < tolerance && fabs(m1.dx() - m2.dx()) < tolerance && fabs(m1.dy() - m2.dy()) < tolerance; } static JXFORM_CODE findJxform(const QTransform& matrix) { OrientationInfoList::ConstIterator it(orientationInfoList().begin()), end(orientationInfoList().end()); for (; it != end; ++it) { if (matricesAreSame((*it).matrix, matrix, 0.001)) { return (*it).jxform; } } qWarning() << "findJxform: failed\n"; return JXFORM_NONE; } void JpegContent::applyPendingTransformation() { if (d->mRawData.size() == 0) { qCritical() << "No data loaded\n"; return; } // The following code is inspired by jpegtran.c from the libjpeg // Init JPEG structs struct jpeg_decompress_struct srcinfo; struct jpeg_compress_struct dstinfo; jvirt_barray_ptr * src_coef_arrays; jvirt_barray_ptr * dst_coef_arrays; // Initialize the JPEG decompression object JPEGErrorManager srcErrorManager; srcinfo.err = &srcErrorManager; jpeg_create_decompress(&srcinfo); if (setjmp(srcErrorManager.jmp_buffer)) { qCritical() << "libjpeg error in src\n"; return; } // Initialize the JPEG compression object JPEGErrorManager dstErrorManager; dstinfo.err = &dstErrorManager; jpeg_create_compress(&dstinfo); if (setjmp(dstErrorManager.jmp_buffer)) { qCritical() << "libjpeg error in dst\n"; return; } // Specify data source for decompression QBuffer buffer(&d->mRawData); buffer.open(QIODevice::ReadOnly); IODeviceJpegSourceManager::setup(&srcinfo, &buffer); // Enable saving of extra markers that we want to copy jcopy_markers_setup(&srcinfo, JCOPYOPT_ALL); (void) jpeg_read_header(&srcinfo, true); // Init transformation jpeg_transform_info transformoption; memset(&transformoption, 0, sizeof(jpeg_transform_info)); transformoption.transform = findJxform(d->mTransformMatrix); jtransform_request_workspace(&srcinfo, &transformoption); /* 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); /* Adjust destination parameters if required by transform options; * also find out which set of coefficient arrays will hold the output. */ dst_coef_arrays = jtransform_adjust_parameters(&srcinfo, &dstinfo, src_coef_arrays, &transformoption); /* Specify data destination for compression */ QByteArray output; output.resize(d->mRawData.size()); d->setupInmemDestination(&dstinfo, &output); /* Start compressor (note no image data is actually written here) */ 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, JCOPYOPT_ALL); /* Execute image transformation, if any */ 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); // Set rawData to our new JPEG d->mRawData = output; } QImage JpegContent::thumbnail() const { QImage image; if (!d->mExifData.empty()) { #if(EXIV2_TEST_VERSION(0,17,91)) Exiv2::ExifThumbC thumb(d->mExifData); Exiv2::DataBuf thumbnail = thumb.copy(); #else Exiv2::DataBuf thumbnail = d->mExifData.copyThumbnail(); #endif image.loadFromData(thumbnail.pData_, thumbnail.size_); Exiv2::ExifData::iterator it = d->mExifData.findKey(Exiv2::ExifKey("Exif.Canon.ThumbnailImageValidArea")); // ensure ThumbnailImageValidArea actually specifies a rectangle, i.e. there must be 4 coordinates if (it != d->mExifData.end() && it->count() == 4) { QRect validArea(QPoint(it->toLong(0), it->toLong(2)), QPoint(it->toLong(1), it->toLong(3))); image = image.copy(validArea); } else { // Unfortunately, Sony does not provide an exif tag that specifies the valid area of the // embedded thumbnail. Need to derive it from the size of the preview image instead. it = d->mExifData.findKey(Exiv2::ExifKey("Exif.Sony1.PreviewImageSize")); if (it != d->mExifData.end() && it->count() == 2) { const long prevHeight = it->toLong(0); const long prevWidth = it->toLong(1); const double scale = prevWidth / image.width(); // the embedded thumb only needs to be cropped vertically const long validThumbAreaHeight = ceil(prevHeight / scale); const long totalHeightOfBlackArea = image.height() - validThumbAreaHeight; // black bars on top and bottom should be equal in height const long offsetFromTop = totalHeightOfBlackArea / 2; const QRect validArea(QPoint(0, offsetFromTop), QSize(image.width(), validThumbAreaHeight)); image = image.copy(validArea); } } Orientation o = orientation(); if (GwenviewConfig::applyExifOrientation() && o != NORMAL && o != NOT_AVAILABLE) { image = image.transformed(ImageUtils::transformMatrix(o)); } } return image; } void JpegContent::setThumbnail(const QImage& thumbnail) { if (d->mExifData.empty()) { return; } QByteArray array; QBuffer buffer(&array); buffer.open(QIODevice::WriteOnly); QImageWriter writer(&buffer, "JPEG"); if (!writer.write(thumbnail)) { qCritical() << "Could not write thumbnail\n"; return; } #if (EXIV2_TEST_VERSION(0,17,91)) Exiv2::ExifThumb thumb(d->mExifData); thumb.setJpegThumbnail((unsigned char*)array.data(), array.size()); #else d->mExifData.setJpegThumbnail((unsigned char*)array.data(), array.size()); #endif } bool JpegContent::save(const QString& path) { // we need to take ownership of the input file's data // if the input file is still open, data is still only mem-mapped if (d->mFile.isOpen()) { // backup the mmap() pointer auto* mappedFile = reinterpret_cast(d->mRawData.data()); // read the file to memory d->mRawData = d->mFile.readAll(); d->mFile.unmap(mappedFile); d->mFile.close(); } QFile file(path); if (!file.open(QIODevice::WriteOnly)) { d->mErrorString = i18nc("@info", "Could not open file for writing."); return false; } return save(&file); } bool JpegContent::save(QIODevice* device) { if (!d->mImage.isNull()) { if (!d->updateRawDataFromImage()) { return false; } } if (d->mRawData.size() == 0) { d->mErrorString = i18nc("@info", "No data to store."); return false; } if (d->mPendingTransformation) { applyPendingTransformation(); d->mPendingTransformation = false; } std::unique_ptr image; image.reset(Exiv2::ImageFactory::open((unsigned char*)d->mRawData.data(), d->mRawData.size()).release()); // Store Exif info image->setExifData(d->mExifData); image->setComment(d->mComment.toUtf8().toStdString()); image->writeMetadata(); // Update mRawData Exiv2::BasicIo& io = image->io(); d->mRawData.resize(io.size()); io.read((unsigned char*)d->mRawData.data(), io.size()); QDataStream stream(device); stream.writeRawData(d->mRawData.data(), d->mRawData.size()); // Make sure we are up to date loadFromData(d->mRawData); return true; } QString JpegContent::errorString() const { return d->mErrorString; } void JpegContent::setImage(const QImage& image) { d->mRawData.clear(); d->mImage = image; d->mSize = image.size(); d->mExifData["Exif.Photo.PixelXDimension"] = image.width(); d->mExifData["Exif.Photo.PixelYDimension"] = image.height(); resetOrientation(); d->mPendingTransformation = false; d->mTransformMatrix = QTransform(); } } // namespace