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 @@
GeneralConfigPage00470338Videos:Show videos
-
+ Qt::VerticalQSizePolicy::Fixed2020Background color:2560255Qt::Horizontal00400trueQFrame::StyledPanelQFrame::SunkenQt::Horizontal4020Qt::VerticalQSizePolicy::Fixed2020
+
+
+ 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.hlib/zoommode.hlib/thumbnailactions.hlib/mousewheelbehavior.hlib/documentview/documentview.hlib/documentview/rasterimageview.hlib/print/printoptionspage.hlib/renderingintent.hGeneral.Name,General.ImageSize,Exif.Photo.ExposureTime,Exif.Photo.Flashtruetruefalse100true0.5The percentage of memory used by Gwenview before it
warns the user and suggest saving changes.newA 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
+
+
Horizontal1falsefalseinformationThumbnailActions::AllButtonsGeneral.Name,Exif.Image.DateTimetruefalseAbstractImageView::AlphaBackgroundNone#ffffffMouseWheelBehavior::Scrollfalsetrue350, 100DocumentView::SoftwareAnimationZoomMode::AutofitDefines 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::PerceptualDefines 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.1283./2.falseSorting::Namefalse1true
Qt::AlignHCenter | Qt::AlignVCenter
PrintOptionsPage::ScaleToPagefalse15.010.0PrintOptionsPage::Centimeterstruefalsetruefalsefalse5.024falsefalse-100truetruefalse
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