You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
267 lines
10 KiB
267 lines
10 KiB
#include "thumbnailjob.h"
|
|
#include <string>
|
|
#include <memory>
|
|
#include <algorithm>
|
|
#include <libexif/exif-loader.h>
|
|
#include <QImageReader>
|
|
#include <QDir>
|
|
#include "thumbnailer.h"
|
|
|
|
namespace Fm {
|
|
|
|
QThreadPool* ThumbnailJob::threadPool_ = nullptr;
|
|
|
|
bool ThumbnailJob::localFilesOnly_ = true;
|
|
int ThumbnailJob::maxThumbnailFileSize_ = 0;
|
|
|
|
ThumbnailJob::ThumbnailJob(FileInfoList files, int size):
|
|
files_{std::move(files)},
|
|
size_{size},
|
|
md5Calc_{g_checksum_new(G_CHECKSUM_MD5)} {
|
|
}
|
|
|
|
ThumbnailJob::~ThumbnailJob() {
|
|
g_checksum_free(md5Calc_);
|
|
// qDebug("delete ThumbnailJob");
|
|
}
|
|
|
|
void ThumbnailJob::exec() {
|
|
for(auto& file: files_) {
|
|
if(isCancelled()) {
|
|
break;
|
|
}
|
|
auto image = loadForFile(file);
|
|
Q_EMIT thumbnailLoaded(file, size_, image);
|
|
results_.emplace_back(std::move(image));
|
|
}
|
|
}
|
|
|
|
QImage ThumbnailJob::readImageFromStream(GInputStream* stream, size_t len) {
|
|
// FIXME: should we set a limit here? Otherwise if len is too large, we can run out of memory.
|
|
std::unique_ptr<unsigned char[]> buffer{new unsigned char[len]}; // allocate enough buffer
|
|
unsigned char* pbuffer = buffer.get();
|
|
size_t totalReadSize = 0;
|
|
while(!isCancelled() && totalReadSize < len) {
|
|
size_t bytesToRead = totalReadSize + 4096 > len ? len - totalReadSize : 4096;
|
|
gssize readSize = g_input_stream_read(stream, pbuffer, bytesToRead, cancellable_.get(), nullptr);
|
|
if(readSize == 0) { // end of file
|
|
break;
|
|
}
|
|
else if(readSize == -1) { // error
|
|
return QImage();
|
|
}
|
|
totalReadSize += readSize;
|
|
pbuffer += readSize;
|
|
}
|
|
QImage image;
|
|
image.loadFromData(buffer.get(), totalReadSize);
|
|
return image;
|
|
}
|
|
|
|
QImage ThumbnailJob::loadForFile(const std::shared_ptr<const FileInfo> &file) {
|
|
if(!file->canThumbnail()) {
|
|
return QImage();
|
|
}
|
|
|
|
// thumbnails are stored in $XDG_CACHE_HOME/thumbnails/large|normal|failed
|
|
QString thumbnailDir{g_get_user_cache_dir()};
|
|
thumbnailDir += "/thumbnails/";
|
|
|
|
// don't make thumbnails for files inside the thumbnail directory
|
|
if(FilePath::fromLocalPath(thumbnailDir.toLocal8Bit().constData()).isParentOf(file->dirPath())) {
|
|
return QImage();
|
|
}
|
|
|
|
const char* subdir = size_ > 128 ? "large" : "normal";
|
|
thumbnailDir += subdir;
|
|
|
|
// generate base name of the thumbnail => {md5 of uri}.png
|
|
auto origPath = file->path();
|
|
auto uri = origPath.uri();
|
|
|
|
char thumbnailName[32 + 5];
|
|
// calculate md5 hash for the uri of the original file
|
|
g_checksum_update(md5Calc_, reinterpret_cast<const unsigned char*>(uri.get()), -1);
|
|
memcpy(thumbnailName, g_checksum_get_string(md5Calc_), 32);
|
|
mempcpy(thumbnailName + 32, ".png", 5);
|
|
g_checksum_reset(md5Calc_); // reset the checksum calculator for next use
|
|
|
|
QString thumbnailFilename = thumbnailDir;
|
|
thumbnailFilename += '/';
|
|
thumbnailFilename += thumbnailName;
|
|
// qDebug() << "thumbnail:" << file->getName().c_str() << thumbnailFilename;
|
|
|
|
// try to load the thumbnail file if it exists
|
|
QImage thumbnail{thumbnailFilename};
|
|
if(thumbnail.isNull() || isThumbnailOutdated(file, thumbnail)) {
|
|
// the existing thumbnail cannot be loaded, generate a new one
|
|
|
|
// create the thumbnail dir as needd (FIXME: Qt file I/O is slow)
|
|
QDir().mkpath(thumbnailDir);
|
|
|
|
thumbnail = generateThumbnail(file, origPath, uri.get(), thumbnailFilename);
|
|
}
|
|
// resize to the size we need
|
|
if(thumbnail.width() > size_ || thumbnail.height() > size_) {
|
|
thumbnail = thumbnail.scaled(size_, size_, Qt::KeepAspectRatio, Qt::SmoothTransformation);
|
|
}
|
|
return thumbnail;
|
|
}
|
|
|
|
bool ThumbnailJob::isSupportedImageType(const std::shared_ptr<const MimeType>& mimeType) const {
|
|
if(mimeType->isImage()) {
|
|
auto supportedTypes = QImageReader::supportedMimeTypes();
|
|
auto found = std::find(supportedTypes.cbegin(), supportedTypes.cend(), mimeType->name());
|
|
if(found != supportedTypes.cend())
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool ThumbnailJob::isThumbnailOutdated(const std::shared_ptr<const FileInfo>& file, const QImage &thumbnail) const {
|
|
QString thumb_mtime = thumbnail.text("Thumb::MTime");
|
|
return (thumb_mtime.isEmpty() || thumb_mtime.toInt() != file->mtime());
|
|
}
|
|
|
|
bool ThumbnailJob::readJpegExif(GInputStream *stream, QImage& thumbnail, int& rotate_degrees) {
|
|
/* try to extract thumbnails embedded in jpeg files */
|
|
ExifLoader* exif_loader = exif_loader_new();
|
|
while(!isCancelled()) {
|
|
unsigned char buf[4096];
|
|
gssize read_size = g_input_stream_read(stream, buf, 4096, cancellable_.get(), nullptr);
|
|
if(read_size <= 0) { // EOF or error
|
|
break;
|
|
}
|
|
if(exif_loader_write(exif_loader, buf, read_size) == 0) {
|
|
break; // no more EXIF data
|
|
}
|
|
}
|
|
ExifData* exif_data = exif_loader_get_data(exif_loader);
|
|
exif_loader_unref(exif_loader);
|
|
if(exif_data) {
|
|
/* reference for EXIF orientation tag:
|
|
* http://www.impulseadventure.com/photo/exif-orientation.html */
|
|
ExifEntry* orient_ent = exif_data_get_entry(exif_data, EXIF_TAG_ORIENTATION);
|
|
if(orient_ent) { /* orientation flag found in EXIF */
|
|
gushort orient;
|
|
ExifByteOrder bo = exif_data_get_byte_order(exif_data);
|
|
/* bo == EXIF_BYTE_ORDER_INTEL ; */
|
|
orient = exif_get_short(orient_ent->data, bo);
|
|
switch(orient) {
|
|
case 1: /* no rotation */
|
|
rotate_degrees = 0;
|
|
break;
|
|
case 8:
|
|
rotate_degrees = 90;
|
|
break;
|
|
case 3:
|
|
rotate_degrees = 180;
|
|
break;
|
|
case 6:
|
|
rotate_degrees = 270;
|
|
break;
|
|
}
|
|
}
|
|
if(exif_data->data) { // if an embedded thumbnail is available, load it
|
|
thumbnail.loadFromData(exif_data->data, exif_data->size);
|
|
}
|
|
exif_data_unref(exif_data);
|
|
}
|
|
return !thumbnail.isNull();
|
|
}
|
|
|
|
QImage ThumbnailJob::generateThumbnail(const std::shared_ptr<const FileInfo>& file, const FilePath& origPath, const char* uri, const QString& thumbnailFilename) {
|
|
QImage result;
|
|
auto mime_type = file->mimeType();
|
|
if(isSupportedImageType(mime_type)) {
|
|
GFileInputStreamPtr ins{g_file_read(origPath.gfile().get(), cancellable_.get(), nullptr), false};
|
|
if(!ins)
|
|
return QImage();
|
|
bool fromExif = false;
|
|
int rotate_degrees = 0;
|
|
if(strcmp(mime_type->name(), "image/jpeg") == 0) { // if this is a jpeg file
|
|
// try to get the thumbnail embedded in EXIF data
|
|
if(readJpegExif(G_INPUT_STREAM(ins.get()), result, rotate_degrees)) {
|
|
fromExif = true;
|
|
}
|
|
}
|
|
if(!fromExif) { // not able to generate a thumbnail from the EXIF data
|
|
// load the original file and do the scaling ourselves
|
|
g_seekable_seek(G_SEEKABLE(ins.get()), 0, G_SEEK_SET, cancellable_.get(), nullptr);
|
|
result = readImageFromStream(G_INPUT_STREAM(ins.get()), file->size());
|
|
}
|
|
g_input_stream_close(G_INPUT_STREAM(ins.get()), nullptr, nullptr);
|
|
|
|
if(!result.isNull()) { // the image is successfully loaded
|
|
// scale the image as needed
|
|
int target_size = size_ > 128 ? 256 : 128;
|
|
|
|
// only scale the original image if it's too large
|
|
if(result.width() > target_size || result.height() > target_size) {
|
|
result = result.scaled(target_size, target_size, Qt::KeepAspectRatio, Qt::SmoothTransformation);
|
|
}
|
|
|
|
if(rotate_degrees != 0) {
|
|
// degree values are 0, 90, 180, and 270 counterclockwise.
|
|
// In Qt, QMatrix does rotation counterclockwise as well.
|
|
// However, because the y axis of widget coordinate system is downward,
|
|
// the real effect of the coordinate transformation becomes clockwise rotation.
|
|
// So we need to use (360 - degree) here.
|
|
// Quote from QMatrix API doc:
|
|
// Note that if you apply a QMatrix to a point defined in widget
|
|
// coordinates, the direction of the rotation will be clockwise because
|
|
// the y-axis points downwards.
|
|
result = result.transformed(QMatrix().rotate(360 - rotate_degrees));
|
|
}
|
|
|
|
// save the generated thumbnail to disk (don't save png thumbnails for JPEG EXIF thumbnails since loading them is cheap)
|
|
if(!fromExif) {
|
|
result.setText("Thumb::MTime", QString::number(file->mtime()));
|
|
result.setText("Thumb::URI", uri);
|
|
result.save(thumbnailFilename, "PNG");
|
|
}
|
|
// qDebug() << "save thumbnail:" << thumbnailFilename;
|
|
}
|
|
}
|
|
else { // the image format is not supported, try to find an external thumbnailer
|
|
// try all available external thumbnailers for it until sucess
|
|
int target_size = size_ > 128 ? 256 : 128;
|
|
file->mimeType()->forEachThumbnailer([&](const std::shared_ptr<const Thumbnailer>& thumbnailer) {
|
|
if(thumbnailer->run(uri, thumbnailFilename.toLocal8Bit().constData(), target_size)) {
|
|
result = QImage(thumbnailFilename);
|
|
}
|
|
return !result.isNull(); // return true on success, and forEachThumbnailer() will stop.
|
|
});
|
|
|
|
if(!result.isNull()) {
|
|
// Some thumbnailers did not write the proper metadata required by the xdg spec to the output (such as evince-thumbnailer)
|
|
// Here we waste some time to fix them so next time we don't need to re-generate these thumbnails. :-(
|
|
bool changed = false;
|
|
if(Q_UNLIKELY(result.text("Thumb::MTime").isEmpty())) {
|
|
result.setText("Thumb::MTime", QString::number(file->mtime()));
|
|
changed = true;
|
|
}
|
|
if(Q_UNLIKELY(result.text("Thumb::URI").isEmpty())) {
|
|
result.setText("Thumb::URI", uri);
|
|
changed = true;
|
|
}
|
|
if(Q_UNLIKELY(changed)) {
|
|
// save the modified PNG file containing metadata to a file.
|
|
result.save(thumbnailFilename, "PNG");
|
|
}
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
QThreadPool* ThumbnailJob::threadPool() {
|
|
if(Q_UNLIKELY(threadPool_ == nullptr)) {
|
|
threadPool_ = new QThreadPool();
|
|
threadPool_->setMaxThreadCount(1);
|
|
}
|
|
return threadPool_;
|
|
}
|
|
|
|
|
|
} // namespace Fm
|