From ca829235d3da7710bf304ea72965af1c21ac3324 Mon Sep 17 00:00:00 2001 From: Marko Mattila Date: Fri, 24 May 2013 09:17:11 +0300 Subject: [PATCH] [nemo-transferengine] scaleImageToSize() function added and unit test updated. --- lib/imageoperation.cpp | 75 ++++++++- lib/imageoperation.cpp.autosave | 275 ++++++++++++++++++++++++++++++++ lib/imageoperation.h | 1 + tests/ut_imageoperation.cpp | 33 ++++ tests/ut_imageoperation.h | 1 + 5 files changed, 384 insertions(+), 1 deletion(-) create mode 100644 lib/imageoperation.cpp.autosave diff --git a/lib/imageoperation.cpp b/lib/imageoperation.cpp index d018f4b..58f8334 100644 --- a/lib/imageoperation.cpp +++ b/lib/imageoperation.cpp @@ -31,7 +31,7 @@ #include #include #include - +#include ) /*! \class ImageOperation \brief The ImageOperation class is a helper class to manipulate images. @@ -200,3 +200,76 @@ QString ImageOperation::scaleImage(const QString &sourceFile, qreal scaleFactor, return tmpFile; } + +/*! + Scale image from a \a sourceFile to the \a targetSize. If user gives \a targetFile argument, it is used for + saving the scaled image to that location. + + NOTE: It's hard to predict what will be the actual file size after scaling because it depends on image data. + Therefore it's good to remember that \a targetSize is used by this algorithm, but the final file size + may varie a lot. + + Returns a path to the scaled image. + */ +QString ImageOperation::scaleImageToSize(const QString &sourceFile, quint64 targetSize, const QString &targetFile) +{ + if (targetSize == 0) { + qWarning() << Q_FUNC_INFO << "Target size is 0. Can't scale image to 0 size!"; + return QString(); + } + + if (!QFile::exists(sourceFile)) { + qWarning() << Q_FUNC_INFO << sourceFile << "doesn't exist!"; + return QString(); + } + + QFileInfo f(sourceFile); + quint64 originalSize = f.size(); + if (originalSize <= targetSize) { + qWarning() << Q_FUNC_INFO << "Target size can not be larger than the original size!"; + return QString(); + } + + QString tmpFile = targetFile; + if (tmpFile.isEmpty()) { + tmpFile = uniqueFilePath(sourceFile); + } + + QImage tmpImage(sourceFile); + if (tmpImage.isNull()) { + qWarning() << Q_FUNC_INFO << "NULL original image!"; + return QString(); + } + + // NOTE: We really don't know the size on the disk after scaling and saving + // + // So this is a home made algorithm to downscale image based on give target size using the following + // logic: + // + // 1) First we figure out magic number (a) from the original image size (s) and width (w) and height(h). + // Magic number is basically a combination of the image depth (bits per pixel) and compression: + // a = s / (w * h) + // + // 2) We want the image to be the same aspect ratio (r) than the original image. + // r = w / h + // + // 3) Calculate the new width based on the following formula, where s' is the target size + // w * h * a = s' => + // w * w / r * a = s' => + // w * w = (s' * r) / a => + // w = sqrt( (s' * r) / a ) + + qint32 w = tmpImage.width(); // Width + qint32 h = tmpImage.height(); // Height + qreal r = w / (h * 1.0); // Aspect ratio + qreal a = originalSize / (w * h * 1.0); // The magic number, which combines depth and compression + + quint32 newWidth = qSqrt((targetSize * r) / a); + QImage scaled = tmpImage.scaledToWidth(newWidth, Qt::SmoothTransformation); + + if (!scaled.save(tmpFile)) { + qWarning() << Q_FUNC_INFO << "Failed to save scaled image to temp file!"; + return QString(); + } + return tmpFile; +} diff --git a/lib/imageoperation.cpp.autosave b/lib/imageoperation.cpp.autosave new file mode 100644 index 0000000..58f8334 --- /dev/null +++ b/lib/imageoperation.cpp.autosave @@ -0,0 +1,275 @@ +/**************************************************************************************** +** +** Copyright (C) 2013 Jolla Ltd. +** Contact: Marko Mattila +** All rights reserved. +** +** This file is part of Nemo Transfer Engine package. +** +** You may use this file under the terms of the GNU Lesser General +** Public License version 2.1 as published by the Free Software Foundation +** and appearing in the file license.lgpl included in the packaging +** of this file. +** +** This library is free software; you can redistribute it and/or +** modify it under the terms of the GNU Lesser General Public +** License version 2.1 as published by the Free Software Foundation +** and appearing in the file license.lgpl included in the packaging +** of this file. +** +** This library 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 +** Lesser General Public License for more details. +** +****************************************************************************************/ + +#include "imageoperation.h" +#include + +#include +#include +#include +#include +#include ) +/*! + \class ImageOperation + \brief The ImageOperation class is a helper class to manipulate images. + + This class is meant to be used by share plugins. It can be used for: + \list + \o Removing image metadata + \o Scaling image + \o Create a temp files from the image paths + \endlist +*/ + +/*! + Creates a file path from the \a sourceFilePath to the \a path location. + If the path is not given it uses system default temp path e.g. '/var/tmp'. + This function uses sourceFilePath as a template to generate a temp file name. + + NOTE: This function doesn't create the file, it only generates the file path to + the temporary file. + + Temporary file will be e.g: + + Source file: "/home/nemo/Pictures/img_001.jpg" + Temporary file: "/var/tmp/img_001_0.jpg" + */ +QString ImageOperation::uniqueFilePath(const QString &sourceFilePath, const QString &path) +{ + if (sourceFilePath.isEmpty() || !QFile::exists(sourceFilePath)) { + qWarning() << Q_FUNC_INFO << sourceFilePath << "Doesn't exist or then the path is empty!"; + return QString(); + } + + if (path.isEmpty()) { + qWarning() << Q_FUNC_INFO << "'path' argument is empty!"; + return QString(); + } + + QFileInfo fileInfo(sourceFilePath); + + + // Construct target temp file path first: + QDir dir(path); + QStringList prevFiles = dir.entryList(QStringList() << fileInfo.baseName() + QLatin1String("*"), QDir::Files); + int count = prevFiles.count(); + + // Create temp file with increasing index in a file name e.g. + // /var/temp/img_001_0.jpg, /var/temp/img_001_1.jpg + // In a case there already is a file with the same filename + QString filePath = dir.absolutePath() + QDir::separator(); + QString fileName = fileInfo.baseName() + + QLatin1String("_") + + QString::number(count) + + QLatin1String(".") + + fileInfo.suffix(); + + // This makes sure that we don't generate a file name which already exists. E.g. there are files: + // img_001_0, img_001_1, img_001_2 and img_001 gets deleted. Then this code would generate a + // filename img_001_2 which already exists + while(prevFiles.contains(fileName)) { + ++count; + fileName = fileInfo.baseName() + + QLatin1String("_") + + QString::number(count) + + QLatin1String(".") + + fileInfo.suffix(); + } + + return filePath + fileName; +} + +/*! + Helper method to remove metadata from jpeg files. Only author and location related + metadata will be removed. \a sourceFile is the path to the original file. + + Returns a path to the copy of the image with metadata removed. + */ +QString ImageOperation::removeImageMetadata(const QString &sourceFile) +{ + + if (!QuillMetadata::canRead(sourceFile)) { + qWarning() << Q_FUNC_INFO << "Can't read the source: " << sourceFile; + return QString(); + } + + QString targetFile = uniqueFilePath(sourceFile); + + // Copy image content first + if (!QFile::copy(sourceFile, targetFile)) { + qWarning() << Q_FUNC_INFO << "Failed to copy content!"; + return QString(); + } + + // Get metadata and remove it + QuillMetadata md(sourceFile); + if(!md.isValid()) { + qWarning() << Q_FUNC_INFO << "Invalid metadata"; + return QString(); + } + + // Remove bunch of metadata entries + md.removeEntry(QuillMetadata::Tag_Creator); + md.removeEntry(QuillMetadata::Tag_Subject); + md.removeEntry(QuillMetadata::Tag_Title); + md.removeEntry(QuillMetadata::Tag_City); + md.removeEntry(QuillMetadata::Tag_Country); + md.removeEntry(QuillMetadata::Tag_Location); + md.removeEntry(QuillMetadata::Tag_Description); + md.removeEntry(QuillMetadata::Tag_Regions); + md.removeEntry(QuillMetadata::Tag_Timestamp); + md.removeEntries(QuillMetadata::TagGroup_GPS); + + // Write modified metadata to the target file + if (!md.write(targetFile)) { + qWarning() << Q_FUNC_INFO << "Failed to clear metadata!"; + return QString(); + } + // Return new file with removed metadata + return targetFile; +} + +/*! + Scale image \a sourceFile using \a scaleFactor. The scaled image is stored to the \a targetFile or + if \a targetFile is not given, then a temporary file is created for saving. + + The \a scaleFactor argument must be greater than 0 and less than 1. This function returns path to the + scaled image. Note that if user doesn't specify \a targetFile the scaled image is stored under temp + directory. Nothing guarantees that created file will remain in that diretory forewer so the caller is + reponsible of copying file for more permanent storing. + + Returns a path to the scaled image. + + It is also recommended that if the caller doesn't use the scaled file, which is stored to the temp + directory later, the caller should remove the file. + */ +QString ImageOperation::scaleImage(const QString &sourceFile, qreal scaleFactor, const QString &targetFile) +{ + if ( scaleFactor <= 0.0 || 1.0 <= scaleFactor) { + qWarning() << Q_FUNC_INFO << "Argument scaleFactor needs to be 0 < scale factor < 1"; + return QString(); + } + + if (!QFile::exists(sourceFile)) { + qWarning() << Q_FUNC_INFO << sourceFile << "doesn't exist!"; + return QString(); + } + + QString tmpFile = targetFile; + if (tmpFile.isEmpty()) { + tmpFile = uniqueFilePath(sourceFile); + } + + // Using just basic QImage scale here. We can easily replace this implementation later, if we notice + // performance bottlenecks here. + QImage tmpImg(sourceFile); + if (tmpImg.isNull()) { + qWarning() << Q_FUNC_INFO << "Null source image!"; + return QString(); + } + + QImage scaled = tmpImg.scaled(tmpImg.size() * scaleFactor, Qt::KeepAspectRatio, Qt::SmoothTransformation); + + if (!scaled.save(tmpFile)) { + qWarning() << Q_FUNC_INFO << "Failed to save scaled image to temp file!"; + return QString(); + } + + return tmpFile; +} + +/*! + Scale image from a \a sourceFile to the \a targetSize. If user gives \a targetFile argument, it is used for + saving the scaled image to that location. + + NOTE: It's hard to predict what will be the actual file size after scaling because it depends on image data. + Therefore it's good to remember that \a targetSize is used by this algorithm, but the final file size + may varie a lot. + + Returns a path to the scaled image. + */ +QString ImageOperation::scaleImageToSize(const QString &sourceFile, quint64 targetSize, const QString &targetFile) +{ + if (targetSize == 0) { + qWarning() << Q_FUNC_INFO << "Target size is 0. Can't scale image to 0 size!"; + return QString(); + } + + if (!QFile::exists(sourceFile)) { + qWarning() << Q_FUNC_INFO << sourceFile << "doesn't exist!"; + return QString(); + } + + QFileInfo f(sourceFile); + quint64 originalSize = f.size(); + if (originalSize <= targetSize) { + qWarning() << Q_FUNC_INFO << "Target size can not be larger than the original size!"; + return QString(); + } + + QString tmpFile = targetFile; + if (tmpFile.isEmpty()) { + tmpFile = uniqueFilePath(sourceFile); + } + + QImage tmpImage(sourceFile); + if (tmpImage.isNull()) { + qWarning() << Q_FUNC_INFO << "NULL original image!"; + return QString(); + } + + // NOTE: We really don't know the size on the disk after scaling and saving + // + // So this is a home made algorithm to downscale image based on give target size using the following + // logic: + // + // 1) First we figure out magic number (a) from the original image size (s) and width (w) and height(h). + // Magic number is basically a combination of the image depth (bits per pixel) and compression: + // a = s / (w * h) + // + // 2) We want the image to be the same aspect ratio (r) than the original image. + // r = w / h + // + // 3) Calculate the new width based on the following formula, where s' is the target size + // w * h * a = s' => + // w * w / r * a = s' => + // w * w = (s' * r) / a => + // w = sqrt( (s' * r) / a ) + + qint32 w = tmpImage.width(); // Width + qint32 h = tmpImage.height(); // Height + qreal r = w / (h * 1.0); // Aspect ratio + qreal a = originalSize / (w * h * 1.0); // The magic number, which combines depth and compression + + quint32 newWidth = qSqrt((targetSize * r) / a); + QImage scaled = tmpImage.scaledToWidth(newWidth, Qt::SmoothTransformation); + + if (!scaled.save(tmpFile)) { + qWarning() << Q_FUNC_INFO << "Failed to save scaled image to temp file!"; + return QString(); + } + return tmpFile; +} diff --git a/lib/imageoperation.h b/lib/imageoperation.h index b8b34b5..5900643 100644 --- a/lib/imageoperation.h +++ b/lib/imageoperation.h @@ -36,6 +36,7 @@ class ImageOperation static QString uniqueFilePath(const QString &sourceFilePath, const QString &path = QDir::tempPath()); static QString removeImageMetadata(const QString &sourceFile); static QString scaleImage(const QString &sourceFile, qreal scaleFactor, const QString &targetFile=QString()); + static QString scaleImageToSize(const QString &sourceFile, quint64 targetSize, const QString &targetFile=QString()); }; #endif // IMAGEOPERATION_H diff --git a/tests/ut_imageoperation.cpp b/tests/ut_imageoperation.cpp index 90cd7d0..5d371c1 100644 --- a/tests/ut_imageoperation.cpp +++ b/tests/ut_imageoperation.cpp @@ -130,6 +130,39 @@ void ut_imageoperation::testScale() QFile::remove(result); } + +void ut_imageoperation::testScaleToSize() +{ + QImage img("images/testimage.jpg"); + QVERIFY(!img.isNull()); + + QString filePath("images/testimage.jpg"); + + QString target = ImageOperation::uniqueFilePath(filePath); + QFileInfo f("images/testimage.jpg"); + int targetSize = f.size() * 0.5; + + // Invalid sourceFile -> fail + QCOMPARE(ImageOperation::scaleImageToSize("", targetSize, target), QString()); + + // Valid source file, invalid targetSize -> fail + QCOMPARE(ImageOperation::scaleImageToSize(filePath, -1.0, target), QString()); + QCOMPARE(ImageOperation::scaleImageToSize(filePath, 0, target), QString()); + QCOMPARE(ImageOperation::scaleImageToSize(filePath, targetSize*40, target), QString()); + + // Proper source file, proper scale factor, proper target file + QString result = ImageOperation::scaleImageToSize(filePath, targetSize, target); + QCOMPARE(result, target); + + QImage tImg(result); + QFileInfo f2(result); + QVERIFY(f2.size() <= targetSize); + QVERIFY(tImg.width() < img.width()); + QVERIFY(tImg.height() < img.height()); + QFile::remove(result); +} + + void ut_imageoperation::testDropMetadata() { // NOTE: The test image doesn't contain all metadata fields such as diff --git a/tests/ut_imageoperation.h b/tests/ut_imageoperation.h index f07e5f8..01de8a1 100644 --- a/tests/ut_imageoperation.h +++ b/tests/ut_imageoperation.h @@ -38,6 +38,7 @@ class ut_imageoperation : public QObject private slots: void testScale(); + void testScaleToSize(); void testDropMetadata(); void testUniqueFilePath(); };