imageoperation.cpp 11.8 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
/****************************************************************************************
**
** Copyright (C) 2013 Jolla Ltd.
** Contact: Marko Mattila <marko.mattila@jollamobile.com>
** 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"
28
#include <QuillMetadata>
29 30 31

#include <QFileInfo>
#include <QtDebug>
32
#include <QImageReader>
33
#include <QSize>
34
#include <QtCore/qmath.h>
35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186
/*!
    \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.
187 188
    QImageReader ir(sourceFile);
    if (!ir.canRead()) {
Marko Mattila's avatar
Marko Mattila committed
189
        qWarning() << Q_FUNC_INFO << "Couldn't read source image!";
190 191 192
        return QString();
    }

193 194 195 196 197 198 199
    QSize imageSize(ir.size());
    imageSize = imageSize.scaled(imageSize * scaleFactor, Qt::KeepAspectRatio);
    ir.setScaledSize(imageSize);
    QImage image = ir.read();

    int angle;
    bool mirrored;
Marko Mattila's avatar
Marko Mattila committed
200
    imageOrientation(sourceFile, &angle, &mirrored);
201 202

    if (mirrored) {
Marko Mattila's avatar
Marko Mattila committed
203
        image = image.mirrored(true, false);
204 205 206 207 208 209 210
    }

    if (angle != 0) {
        QTransform transform;
        transform.rotate(angle);
        image = image.transformed(transform);
    }
211

212
    if (!image.save(tmpFile)) {
213 214 215
        qWarning() << Q_FUNC_INFO
                   << "Failed to save scaled image to temp file!"
                   << tmpFile;
216 217 218 219 220
        return QString();
    }

    return tmpFile;
}
221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254

/*!
    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) {
        return QString();
    }

    QString tmpFile = targetFile;
    if (tmpFile.isEmpty()) {
        tmpFile = uniqueFilePath(sourceFile);
    }

255 256 257
    QImageReader ir(sourceFile);
    if (!ir.canRead()) {
        qWarning() << Q_FUNC_INFO << "Can't read the original image!";
258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278
        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 )

279 280
    qint32  w = ir.size().width();              // Width
    qint32  h = ir.size().height();             // Height
281 282 283
    qreal   r = w / (h * 1.0);                  // Aspect ratio
    qreal   a = originalSize / (w * h * 1.0);   // The magic number, which combines depth and compression

Marko Mattila's avatar
Marko Mattila committed
284
    qint32 newWidth = qSqrt((targetSize * r) / a);
285
    qint32 newHeight = newWidth / r;
286 287 288 289 290 291

    QSize imageSize(ir.size());
    imageSize = imageSize.scaled(newWidth, newHeight, Qt::KeepAspectRatio);
    ir.setScaledSize(imageSize);
    QImage image = ir.read();

292 293 294 295 296
    if (image.isNull()) {
        qWarning() << Q_FUNC_INFO
                   << "NULL image";
        return QString();
    }
297 298 299
    // Make sure orientation is right.
    int angle;
    bool mirrored;
Marko Mattila's avatar
Marko Mattila committed
300
    imageOrientation(sourceFile, &angle, &mirrored);
301 302

    if (mirrored) {
Marko Mattila's avatar
Marko Mattila committed
303
        image = image.mirrored(true, false);
304
    }
305

306 307 308 309 310 311 312
    if (angle != 0) {
        QTransform transform;
        transform.rotate(angle);
        image = image.transformed(transform);
    }

    if (!image.save(tmpFile)) {
313 314 315
        qWarning() << Q_FUNC_INFO
                   << "Failed to save scaled image to temp file!"
                   << tmpFile;
316 317
        return QString();
    }
318

319 320
    return tmpFile;
}
321

Marko Mattila's avatar
Marko Mattila committed
322
void ImageOperation::imageOrientation(const QString &sourceFile, int *angle, bool *mirror)
323
{
324 325
    if(!QuillMetadata::canRead(sourceFile)) {
        qWarning() << Q_FUNC_INFO << "Can't read metadata";
Marko Mattila's avatar
Marko Mattila committed
326 327
        *angle = 0;
        *mirror = false;
328 329
        return;
    }
330
    QuillMetadata md(sourceFile);
331 332
    if (!md.hasExif()) {
        qWarning() << "Metadata invalid";
Marko Mattila's avatar
Marko Mattila committed
333 334
        *angle = 0;
        *mirror = false;
335 336 337 338 339
        return;
    }

    int exifOrientation = md.entry(QuillMetadata::Tag_Orientation).toInt();
    switch (exifOrientation) {
Marko Mattila's avatar
Marko Mattila committed
340 341 342 343 344 345 346 347
    case 1: *angle = 0  ; *mirror = false; break;
    case 2: *angle = 0  ; *mirror = true ; break;
    case 3: *angle = 180; *mirror = false; break;
    case 4: *angle = 180; *mirror = true ; break;
    case 5: *angle = 90 ; *mirror = true ; break;
    case 6: *angle = 90 ; *mirror = false; break;
    case 7: *angle = 270; *mirror = true ; break;
    case 8: *angle = 270; *mirror = false; break;
348 349 350
    default: break;
    }
}