Dive pictures: Recognize video files

When generating thumbnails, test for video files. If it is, use
a dummy-thumbnail. Write only the type (video), but no image to
the thumbnail cache, for forward-compatibility.

Signed-off-by: Berthold Stoeger <bstoeger@mail.tuwien.ac.at>
This commit is contained in:
Berthold Stoeger 2018-05-15 20:47:35 +02:00 committed by Dirk Hohndel
parent 9a844a075c
commit b28dba6087
5 changed files with 83 additions and 47 deletions

View file

@ -69,27 +69,40 @@ void ImageDownloader::saveImage(QNetworkReply *reply)
reply->deleteLater(); reply->deleteLater();
} }
// Fetch a picture from the given filename. If this is a non-remote filename, fetch it from disk. static bool isVideoFile(const QString &filename)
// Remote files are fetched from the net in a background thread. In such a case, the output-flag {
// "stillLoading" is set to true. // Currently, we're very crude. Simply check if the file exists and if it
// is an MP4-style format.
QFileInfo fi(filename);
if (!fi.exists() && !fi.isFile())
return false;
metadata md;
return get_metadata(qPrintable(filename), &md) == MEDIATYPE_VIDEO;
}
// Fetch a picture from the given filename and determine its type (picture of video).
// If this is a non-remote file, fetch it from disk. Remote files are fetched from the
// net in a background thread. In such a case, the output-type is set to MEDIATYPE_STILL_LOADING.
// If the input-flag "tryDownload" is set to false, no download attempt is made. This is to // If the input-flag "tryDownload" is set to false, no download attempt is made. This is to
// prevent infinite loops, where failed image downloads would be repeated ad infinitum. // prevent infinite loops, where failed image downloads would be repeated ad infinitum.
// Returns: fetched image, stillLoading flag // Returns: fetched image, type
static std::pair<QImage, bool> fetchImage(const QString &filename, const QString &originalFilename, bool tryDownload) Thumbnailer::Thumbnail Thumbnailer::fetchImage(const QString &filename, const QString &originalFilename, bool tryDownload)
{ {
QImage thumb;
bool stillLoading = false;
QUrl url = QUrl::fromUserInput(filename); QUrl url = QUrl::fromUserInput(filename);
if (url.isLocalFile()) { if (url.isLocalFile()) {
thumb.load(url.toLocalFile()); QString filename = url.toLocalFile();
if (isVideoFile(filename))
return { videoImage, MEDIATYPE_VIDEO };
QImage thumb(filename);
return { thumb, thumb.isNull() ? MEDIATYPE_IO_ERROR : MEDIATYPE_PICTURE };
} else if (tryDownload) { } else if (tryDownload) {
// This has to be done in UI main thread, because QNetworkManager refuses // This has to be done in UI main thread, because QNetworkManager refuses
// to treat requests from other threads. invokeMethod() is Qt's way of calling a // to treat requests from other threads. invokeMethod() is Qt's way of calling a
// function in a different thread, namely the thread the called object is associated to. // function in a different thread, namely the thread the called object is associated to.
QMetaObject::invokeMethod(ImageDownloader::instance(), "load", Qt::AutoConnection, Q_ARG(QUrl, url), Q_ARG(QString, originalFilename)); QMetaObject::invokeMethod(ImageDownloader::instance(), "load", Qt::AutoConnection, Q_ARG(QUrl, url), Q_ARG(QString, originalFilename));
stillLoading = true; return { QImage(), MEDIATYPE_STILL_LOADING };
} }
return { thumb, stillLoading }; return { QImage(), MEDIATYPE_IO_ERROR };
} }
// Fetch a picture based on its original filename. If there is a translated filename (obtained either // Fetch a picture based on its original filename. If there is a translated filename (obtained either
@ -98,25 +111,24 @@ static std::pair<QImage, bool> fetchImage(const QString &filename, const QString
// was downloaded previously, but for some reason the cached picture was lost. Therefore, in such a // was downloaded previously, but for some reason the cached picture was lost. Therefore, in such a
// case, try the canonical filename. If that likewise fails, give up. For input and output parameters // case, try the canonical filename. If that likewise fails, give up. For input and output parameters
// see fetchImage() above. // see fetchImage() above.
static std::pair<QImage, bool> getHashedImage(const QString &filename, bool tryDownload) Thumbnailer::Thumbnail Thumbnailer::getHashedImage(const QString &filename, bool tryDownload)
{ {
QImage thumb;
bool stillLoading = false;
QString localFilename = localFilePath(filename); QString localFilename = localFilePath(filename);
// If there is a translated filename, try that first // If there is a translated filename, try that first
Thumbnail thumbnail { QImage(), MEDIATYPE_UNKNOWN };
if (localFilename != filename) if (localFilename != filename)
std::tie(thumb, stillLoading) = fetchImage(localFilename, filename, tryDownload); thumbnail = fetchImage(localFilename, filename, tryDownload);
// Note that the translated filename should never be a remote file and therefore checking for // Note that the translated filename should never be a remote file and therefore checking for
// stillLoading is currently not necessary. But in the future, we might support such a use case // still-loading is currently not necessary. But in the future, we might support such a use case
// (e.g. images stored in the cloud). // (e.g. images stored in the cloud).
if (thumb.isNull() && !stillLoading) if (thumbnail.img.isNull() && thumbnail.type != MEDIATYPE_STILL_LOADING)
std::tie(thumb, stillLoading) = fetchImage(filename, filename, tryDownload); thumbnail = fetchImage(filename, filename, tryDownload);
if (thumb.isNull() && !stillLoading) if (thumbnail.img.isNull() && thumbnail.type != MEDIATYPE_STILL_LOADING)
qInfo() << "Error loading image" << filename << "[local:" << localFilename << "]"; qInfo() << "Error loading image" << filename << "[local:" << localFilename << "]";
return { thumb, stillLoading }; return thumbnail;
} }
static QImage renderIcon(const char *id, int size) static QImage renderIcon(const char *id, int size)
@ -130,7 +142,8 @@ static QImage renderIcon(const char *id, int size)
} }
Thumbnailer::Thumbnailer() : failImage(renderIcon(":filter-close", maxThumbnailSize())), // TODO: Don't misuse filter close icon Thumbnailer::Thumbnailer() : failImage(renderIcon(":filter-close", maxThumbnailSize())), // TODO: Don't misuse filter close icon
dummyImage(renderIcon(":camera-icon", maxThumbnailSize())) dummyImage(renderIcon(":camera-icon", maxThumbnailSize())),
videoImage(renderIcon(":video-icon", maxThumbnailSize()))
{ {
// Currently, we only process one image at a time. Stefan Fuchs reported problems when // Currently, we only process one image at a time. Stefan Fuchs reported problems when
// calculating multiple thumbnails at once and this hopefully helps. // calculating multiple thumbnails at once and this hopefully helps.
@ -145,11 +158,11 @@ Thumbnailer *Thumbnailer::instance()
return &self; return &self;
} }
static QImage getThumbnailFromCache(const QString &picture_filename) Thumbnailer::Thumbnail Thumbnailer::getThumbnailFromCache(const QString &picture_filename)
{ {
QString filename = thumbnailFileName(picture_filename); QString filename = thumbnailFileName(picture_filename);
if (filename.isEmpty()) if (filename.isEmpty())
return QImage(); return { QImage(), MEDIATYPE_UNKNOWN };
QFile file(filename); QFile file(filename);
if (prefs.auto_recalculate_thumbnails) { if (prefs.auto_recalculate_thumbnails) {
@ -163,27 +176,32 @@ static QImage getThumbnailFromCache(const QString &picture_filename)
if (pictureTime.isValid() && thumbnailTime.isValid() && thumbnailTime < pictureTime) { if (pictureTime.isValid() && thumbnailTime.isValid() && thumbnailTime < pictureTime) {
// Both files exist, have valid timestamps and thumbnail was calculated before picture. // Both files exist, have valid timestamps and thumbnail was calculated before picture.
// Return an empty thumbnail to signal recalculation of the thumbnail // Return an empty thumbnail to signal recalculation of the thumbnail
return QImage(); return { QImage(), MEDIATYPE_UNKNOWN };
} }
} }
} }
if (!file.open(QIODevice::ReadOnly)) if (!file.open(QIODevice::ReadOnly))
return QImage(); return { QImage(), MEDIATYPE_UNKNOWN };
QDataStream stream(&file); QDataStream stream(&file);
// Each thumbnail file is composed of a media-type and an image file. // Each thumbnail file is composed of a media-type and an image file.
// Currently, the type is ignored. This will be used to mark videos.
quint32 type; quint32 type;
QImage res; QImage res;
stream >> type; stream >> type;
stream >> res; stream >> res;
return res;
// Thumbnails of videos currently not supported - replace by dummy
// TODO: Perhaps extract thumbnails
if (type == MEDIATYPE_VIDEO)
res = videoImage;
return { res, (mediatype_t)type };
} }
static void addThumbnailToCache(const QImage &thumbnail, const QString &picture_filename) void Thumbnailer::addThumbnailToCache(const Thumbnail &thumbnail, const QString &picture_filename)
{ {
if (thumbnail.isNull()) if (thumbnail.img.isNull())
return; return;
QString filename = thumbnailFileName(picture_filename); QString filename = thumbnailFileName(picture_filename);
@ -192,51 +210,48 @@ static void addThumbnailToCache(const QImage &thumbnail, const QString &picture_
return; return;
QDataStream stream(&file); QDataStream stream(&file);
// For format of the file, see comments in getThumnailForCache stream << (quint32)thumbnail.type;
quint32 type = MEDIATYPE_PICTURE; if (thumbnail.type == MEDIATYPE_PICTURE) // TODO: Perhaps also support caching of video thumbnails
stream << type; stream << thumbnail.img;
stream << thumbnail;
file.commit(); file.commit();
} }
void Thumbnailer::recalculate(QString filename) void Thumbnailer::recalculate(QString filename)
{ {
auto res = getHashedImage(filename, true); Thumbnail thumbnail = getHashedImage(filename, true);
// If we couldn't load the image from disk -> leave old thumbnail. // If we couldn't load the image from disk -> leave old thumbnail.
// The case "load from web" is a bit inconsistent: it will call into processItem() later // The case "load from web" is a bit inconsistent: it will call into processItem() later
// and therefore a "broken" image symbol may be shown. // and therefore a "broken" image symbol may be shown.
if (res.second || res.first.isNull()) if (thumbnail.type == MEDIATYPE_STILL_LOADING || thumbnail.img.isNull())
return; return;
QImage thumbnail = res.first;
addThumbnailToCache(thumbnail, filename); addThumbnailToCache(thumbnail, filename);
QMutexLocker l(&lock); QMutexLocker l(&lock);
emit thumbnailChanged(filename, thumbnail); emit thumbnailChanged(filename, thumbnail.img);
workingOn.remove(filename); workingOn.remove(filename);
} }
void Thumbnailer::processItem(QString filename, bool tryDownload) void Thumbnailer::processItem(QString filename, bool tryDownload)
{ {
QImage thumbnail = getThumbnailFromCache(filename); Thumbnail thumbnail = getThumbnailFromCache(filename);
if (thumbnail.isNull()) { if (thumbnail.img.isNull()) {
auto res = getHashedImage(filename, tryDownload); thumbnail = getHashedImage(filename, tryDownload);
if (res.second) if (thumbnail.type == MEDIATYPE_STILL_LOADING)
return; return;
thumbnail = res.first;
if (thumbnail.isNull()) { if (thumbnail.img.isNull()) {
thumbnail = failImage; thumbnail.img = failImage;
} else { } else {
int size = maxThumbnailSize(); int size = maxThumbnailSize();
thumbnail = thumbnail.scaled(size, size, Qt::KeepAspectRatio); thumbnail.img = thumbnail.img.scaled(size, size, Qt::KeepAspectRatio);
addThumbnailToCache(thumbnail, filename); addThumbnailToCache(thumbnail, filename);
} }
} }
QMutexLocker l(&lock); QMutexLocker l(&lock);
emit thumbnailChanged(filename, thumbnail); emit thumbnailChanged(filename, thumbnail.img);
workingOn.remove(filename); workingOn.remove(filename);
} }

View file

@ -2,6 +2,7 @@
#ifndef IMAGEDOWNLOADER_H #ifndef IMAGEDOWNLOADER_H
#define IMAGEDOWNLOADER_H #define IMAGEDOWNLOADER_H
#include "metadata.h"
#include <QImage> #include <QImage>
#include <QFuture> #include <QFuture>
#include <QNetworkReply> #include <QNetworkReply>
@ -48,14 +49,24 @@ public slots:
signals: signals:
void thumbnailChanged(QString filename, QImage thumbnail); void thumbnailChanged(QString filename, QImage thumbnail);
private: private:
struct Thumbnail {
QImage img;
mediatype_t type;
};
Thumbnailer(); Thumbnailer();
static void addThumbnailToCache(const Thumbnail &thumbnail, const QString &picture_filename);
void recalculate(QString filename); void recalculate(QString filename);
void processItem(QString filename, bool tryDownload); void processItem(QString filename, bool tryDownload);
Thumbnail getThumbnailFromCache(const QString &picture_filename);
Thumbnail fetchImage(const QString &filename, const QString &originalFilename, bool tryDownload);
Thumbnail getHashedImage(const QString &filename, bool tryDownload);
mutable QMutex lock; mutable QMutex lock;
QThreadPool pool; QThreadPool pool;
QImage failImage; // Shown when image-fetching fails QImage failImage; // Shown when image-fetching fails
QImage dummyImage; // Shown before thumbnail is fetched QImage dummyImage; // Shown before thumbnail is fetched
QImage videoImage; // Place holder for videos
QMap<QString,QFuture<void>> workingOn; QMap<QString,QFuture<void>> workingOn;
}; };

View file

@ -10,10 +10,11 @@ struct metadata {
}; };
enum mediatype_t { enum mediatype_t {
MEDIATYPE_IO_ERROR, // Couldn't read file MEDIATYPE_UNKNOWN, // Couldn't (yet) identify file
MEDIATYPE_UNKNOWN, // Couldn't identify file MEDIATYPE_IO_ERROR, // Couldn't read file
MEDIATYPE_PICTURE, MEDIATYPE_PICTURE,
MEDIATYPE_VIDEO, MEDIATYPE_VIDEO,
MEDIATYPE_STILL_LOADING, // Still processing in the background
}; };
#ifdef __cplusplus #ifdef __cplusplus

View file

@ -1209,11 +1209,19 @@ QString localFilePath(const QString &originalFilename)
return localFilenameOf.value(originalFilename, originalFilename); return localFilenameOf.value(originalFilename, originalFilename);
} }
// TODO: Apparently Qt has no simple way of listing the supported video
// codecs? Do we have to query them by hand using QMediaPlayer::hasSupport()?
const QStringList videoExtensionsList = {
".avi", ".mp4", ".mpeg", ".mpg", ".wmv"
};
QStringList imageExtensionFilters() { QStringList imageExtensionFilters() {
QStringList filters; QStringList filters;
foreach (QString format, QImageReader::supportedImageFormats()) { foreach (QString format, QImageReader::supportedImageFormats()) {
filters.append(QString("*.").append(format)); filters.append(QString("*.").append(format));
} }
foreach (const QString &format, videoExtensionsList)
filters.append("*" + format);
return filters; return filters;
} }

View file

@ -42,6 +42,7 @@ QByteArray getCurrentAppState();
void setCurrentAppState(const QByteArray &state); void setCurrentAppState(const QByteArray &state);
void init_proxy(); void init_proxy();
QString getUUID(); QString getUUID();
extern const QStringList videoExtensionsList;
QStringList imageExtensionFilters(); QStringList imageExtensionFilters();
char *intdup(int index); char *intdup(int index);
char *copy_qstring(const QString &); char *copy_qstring(const QString &);