diff --git a/CMakeLists.txt b/CMakeLists.txt --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -111,6 +111,14 @@ PURPOSE "Needed to build the MTP kioslave" ) +find_package(libmagic) +set_package_properties(libmagic PROPERTIES DESCRIPTION "The libmagic library" + URL "https://www.darwinsys.com/file/" + TYPE OPTIONAL + PURPOSE "Optional to improve the text thumbnail creator encoding detection" + ) + + check_include_file(utime.h HAVE_UTIME_H) # ECM's KDECompilerSettings.cmake should take care of enabling supporting on diff --git a/cmake/Findlibmagic.cmake b/cmake/Findlibmagic.cmake new file mode 100644 --- /dev/null +++ b/cmake/Findlibmagic.cmake @@ -0,0 +1,65 @@ +# - Try to find libssh +# Once done this will define +# +# LIBMAGIC_FOUND - system has libmagic +# LIBMAGIC_INCLUDE_DIR - the libmagic include directory +# LIBMAGIC_LIBRARIES - Link these to use libmagic +# LIBMAGIC_DEFINITIONS - Compiler switches required for using libmagic +# +# Copyright (c) 2020 Méven Car +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# 3. Neither the name of the University nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +# SUCH DAMAGE. + + +if (LIBMAGIC_INCLUDE_DIR AND LIBMAGIC_LIBRARIES) + + # in cache already + SET(LIBMAGIC_FOUND TRUE) + +else (LIBMAGIC_INCLUDE_DIR AND LIBMAGIC_LIBRARIES) + if(NOT WIN32) + # use pkg-config to get the directories and then use these values + # in the FIND_PATH() and FIND_LIBRARY() calls + INCLUDE(FindPkgConfig) + + pkg_check_modules(_LIBMAGIC libmagic) + + set(LIBMAGIC_DEFINITIONS ${_LIBMAGIC_CFLAGS}) + endif(NOT WIN32) + FIND_PATH(LIBMAGIC_INCLUDE_DIR magic.h + ${_LIBMAGIC_INCLUDE_DIRS} + ) + + FIND_LIBRARY(LIBMAGIC_LIBRARIES NAMES libmagic + PATHS + ${_LIBMAGIC_LIBRARY_DIRS} + ) + + if (LIBMAGIC_INCLUDE_DIR AND LIBMAGIC_LIBRARIES) + set(LIBMAGIC_FOUND TRUE) + endif (LIBMAGIC_INCLUDE_DIR AND LIBMAGIC_LIBRARIES) + + +endif (LIBMAGIC_INCLUDE_DIR AND LIBMAGIC_LIBRARIES) diff --git a/thumbnail/CMakeLists.txt b/thumbnail/CMakeLists.txt --- a/thumbnail/CMakeLists.txt +++ b/thumbnail/CMakeLists.txt @@ -103,6 +103,10 @@ KF5::KIOWidgets KF5::SyntaxHighlighting ) +configure_file(config-thumbnail.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/config-thumbnail.h ) +if (LIBMAGIC_FOUND) + target_link_libraries(textthumbnail magic) +endif() install(TARGETS textthumbnail DESTINATION ${KDE_INSTALL_PLUGINDIR}) diff --git a/thumbnail/config-thumbnail.h.cmake b/thumbnail/config-thumbnail.h.cmake new file mode 100644 --- /dev/null +++ b/thumbnail/config-thumbnail.h.cmake @@ -0,0 +1 @@ +#cmakedefine01 LIBMAGIC_FOUND diff --git a/thumbnail/djvucreator.h b/thumbnail/djvucreator.h --- a/thumbnail/djvucreator.h +++ b/thumbnail/djvucreator.h @@ -23,11 +23,11 @@ #include -class DjVuCreator : public ThumbCreator +class DjVuCreator : public ThumbCreatorV3 { public: DjVuCreator() {} - bool create(const QString &path, int, int, QImage &img) override; + bool createV3(const QString &path, int, int, QImage &img, qreal devicePixelRatio) override; Flags flags() const override; }; diff --git a/thumbnail/djvucreator.cpp b/thumbnail/djvucreator.cpp --- a/thumbnail/djvucreator.cpp +++ b/thumbnail/djvucreator.cpp @@ -47,7 +47,7 @@ } } -bool DjVuCreator::create(const QString &path, int width, int height, QImage &img) +bool DjVuCreator::createV3(const QString &path, int width, int height, QImage &img, qreal devicePixelRatio) { int output[2]; QByteArray data(1024, 'k'); @@ -58,7 +58,7 @@ const char* argv[8]; QByteArray sizearg, fnamearg; - sizearg = QByteArray::number(width) + 'x' + QByteArray::number(height); + sizearg = QByteArray::number(width * devicePixelRatio) + 'x' + QByteArray::number(height * devicePixelRatio); fnamearg = QFile::encodeName( path ); argv[0] = "ddjvu"; argv[1] = "-page"; @@ -121,6 +121,17 @@ close(output[0]); int l = img.loadFromData( data ); + if (!img.isNull()) { + + const int maxWidth = static_cast(width * devicePixelRatio); + const int maxHeight = static_cast(height * devicePixelRatio); + + const QSize imageSize = img.size(); + if (imageSize.isValid() && (imageSize.width() > maxWidth || imageSize.height() > maxHeight)) { + img = img.scaled(maxWidth, maxHeight, Qt::KeepAspectRatio, Qt::SmoothTransformation); + } + img.setDevicePixelRatio(devicePixelRatio); + } return ok && l; } diff --git a/thumbnail/imagecreator.h b/thumbnail/imagecreator.h --- a/thumbnail/imagecreator.h +++ b/thumbnail/imagecreator.h @@ -24,11 +24,11 @@ #include -class ImageCreator : public ThumbCreator +class ImageCreator : public ThumbCreatorV3 { public: ImageCreator() {} - bool create(const QString &path, int, int, QImage &img) override; + bool createV3(const QString &path, int, int, QImage &img, qreal devicePixelRatio) override; Flags flags() const override; }; diff --git a/thumbnail/imagecreator.cpp b/thumbnail/imagecreator.cpp --- a/thumbnail/imagecreator.cpp +++ b/thumbnail/imagecreator.cpp @@ -30,14 +30,27 @@ } } -bool ImageCreator::create(const QString &path, int, int, QImage &img) +bool ImageCreator::createV3(const QString &path, int width, int height, QImage &img, qreal devicePixelRatio) { // create image preview QImageReader ir(path); ir.setDecideFormatFromContent(true); + + const int maxWidth = width * devicePixelRatio; + const int maxHeight = height * devicePixelRatio; + + QSize imageSize = ir.size(); + if (imageSize.isValid() && (imageSize.width() > maxWidth || imageSize.height() > maxHeight)) { + const QSize thumbnailSize = imageSize.scaled(maxWidth, maxHeight, Qt::KeepAspectRatio); + ir.setScaledSize(thumbnailSize); // fast downscaling + } + ir.setQuality(75); // set quality so that the jpeg handler will use a high quality downscaler + img = ir.read(); if (img.isNull()) return false; + + img.setDevicePixelRatio(devicePixelRatio); if (img.depth() != 32) img = img.convertToFormat(img.hasAlphaChannel() ? QImage::Format_ARGB32 : QImage::Format_RGB32); return true; diff --git a/thumbnail/jpegcreator.h b/thumbnail/jpegcreator.h --- a/thumbnail/jpegcreator.h +++ b/thumbnail/jpegcreator.h @@ -24,11 +24,11 @@ class QTransform; -class JpegCreator : public ThumbCreator +class JpegCreator : public ThumbCreatorV3 { public: JpegCreator(); - bool create(const QString &path, int, int, QImage &img) override; + bool createV3(const QString &path, int, int, QImage &img, qreal devicePixelRatio) override; Flags flags() const override; QWidget *createConfigurationWidget() override; void writeConfiguration(const QWidget *configurationWidget) override; diff --git a/thumbnail/jpegcreator.cpp b/thumbnail/jpegcreator.cpp --- a/thumbnail/jpegcreator.cpp +++ b/thumbnail/jpegcreator.cpp @@ -38,22 +38,29 @@ { } -bool JpegCreator::create(const QString &path, int width, int height, QImage &image) +bool JpegCreator::createV3(const QString &path, int width, int height, QImage &image, qreal devicePixelRatio) { QImageReader imageReader(path, "jpeg"); + const int maxWidth = static_cast(width * devicePixelRatio); + const int maxHeight = static_cast(height * devicePixelRatio); + const QSize imageSize = imageReader.size(); - if (imageSize.isValid() && (imageSize.width() > width || imageSize.height() > height)) { - const QSize thumbnailSize = imageSize.scaled(width, height, Qt::KeepAspectRatio); + if (imageSize.isValid() && (imageSize.width() > maxWidth || imageSize.height() > maxHeight)) { + const QSize thumbnailSize = imageSize.scaled(maxWidth, maxHeight, Qt::KeepAspectRatio); imageReader.setScaledSize(thumbnailSize); // fast downscaling } imageReader.setQuality(75); // set quality so that the jpeg handler will use a high quality downscaler JpegCreatorSettings* settings = JpegCreatorSettings::self(); settings->load(); imageReader.setAutoTransform(settings->rotate()); - return imageReader.read(&image); + bool result = imageReader.read(&image); + if (result) { + image.setDevicePixelRatio(devicePixelRatio); + } + return result; } ThumbCreator::Flags JpegCreator::flags() const diff --git a/thumbnail/kritacreator.h b/thumbnail/kritacreator.h --- a/thumbnail/kritacreator.h +++ b/thumbnail/kritacreator.h @@ -28,13 +28,13 @@ /** * The Krita thumbnail creator can create thumbnails for krita and openraster images */ -class KritaCreator : public ThumbCreator +class KritaCreator : public ThumbCreatorV3 { public: KritaCreator(); ~KritaCreator() override; - bool create(const QString &path, int width, int height, QImage &image) override; + bool createV3(const QString &path, int width, int height, QImage &image, qreal devicePixelRatio) override; Flags flags() const override; }; diff --git a/thumbnail/kritacreator.cpp b/thumbnail/kritacreator.cpp --- a/thumbnail/kritacreator.cpp +++ b/thumbnail/kritacreator.cpp @@ -43,7 +43,7 @@ { } -bool KritaCreator::create(const QString &path, int width, int height, QImage &image) +bool KritaCreator::createV3(const QString &path, int width, int height, QImage &image, qreal devicePixelRatio) { // for now just rely on the rendered data inside the file, // do not load Krita code for rendering ourselves, as that currently (2.9) @@ -71,7 +71,7 @@ const KZipFileEntry* fileZipEntry = static_cast(entry); bool thumbLoaded = thumbnail.loadFromData(fileZipEntry->data(), "PNG"); if (thumbLoaded) { - biggerSizeNeeded = (thumbnail.width() < width || thumbnail.height() < height); + biggerSizeNeeded = (thumbnail.width() < width * devicePixelRatio || thumbnail.height() < height * devicePixelRatio); } if (biggerSizeNeeded || !thumbLoaded) { @@ -82,7 +82,8 @@ fileZipEntry = static_cast(entry); thumbLoaded = thumbnail.loadFromData(fileZipEntry->data(), "PNG"); if (thumbLoaded) { - thumbnail = thumbnail.scaled(width, height, Qt::KeepAspectRatio, Qt::SmoothTransformation); + thumbnail = thumbnail.scaled(width * devicePixelRatio, height * devicePixelRatio, Qt::KeepAspectRatio, Qt::SmoothTransformation); + image.setDevicePixelRatio(devicePixelRatio); } else { return false; } @@ -93,6 +94,7 @@ // (or better checkerboard?) image = QImage(thumbnail.size(), QImage::Format_RGB32); image.fill(QColor(Qt::white).rgb()); + image.setDevicePixelRatio(thumbnail.devicePixelRatio()); QPainter p(&image); p.drawImage(QPoint(0, 0), thumbnail); diff --git a/thumbnail/svgcreator.h b/thumbnail/svgcreator.h --- a/thumbnail/svgcreator.h +++ b/thumbnail/svgcreator.h @@ -22,11 +22,11 @@ #include -class SvgCreator : public ThumbCreator +class SvgCreator : public ThumbCreatorV3 { public: SvgCreator() {} - bool create(const QString &path, int w, int h, QImage &img) override; + bool createV3(const QString &path, int w, int h, QImage &img, qreal devicePixelRatio) override; Flags flags() const override; }; diff --git a/thumbnail/svgcreator.cpp b/thumbnail/svgcreator.cpp --- a/thumbnail/svgcreator.cpp +++ b/thumbnail/svgcreator.cpp @@ -31,25 +31,35 @@ } } -bool SvgCreator::create(const QString &path, int w, int h, QImage &img) +bool SvgCreator::createV3(const QString &path, int width, int height, QImage &img, qreal devicePixelRatio) { QSvgRenderer r(path); if ( !r.isValid() ) return false; // render using the correct ratio const double ratio = static_cast(r.defaultSize().height()) / static_cast(r.defaultSize().width()); - if (w < h) - h = qRound(ratio * w); + if (width < height) + height = qRound(ratio * width); else - w = qRound(h / ratio); + width = qRound(height / ratio); - QImage i(w, h, QImage::Format_ARGB32_Premultiplied); + QImage i(width * devicePixelRatio, height * devicePixelRatio, QImage::Format_ARGB32_Premultiplied); + i.setDevicePixelRatio(devicePixelRatio); i.fill(0); QPainter p(&i); r.render(&p); img = i; + + const int maxWidth = static_cast(width * devicePixelRatio); + const int maxHeight = static_cast(height * devicePixelRatio); + + const QSize imageSize = img.size(); + if (imageSize.isValid() && (imageSize.width() > maxWidth || imageSize.height() > maxHeight)) { + img = img.scaled(maxWidth, maxHeight, Qt::KeepAspectRatio, Qt::SmoothTransformation); + } + return true; } diff --git a/thumbnail/textcreator.h b/thumbnail/textcreator.h --- a/thumbnail/textcreator.h +++ b/thumbnail/textcreator.h @@ -24,12 +24,12 @@ #include #include -class TextCreator : public ThumbCreator +class TextCreator : public ThumbCreatorV3 { public: TextCreator(); ~TextCreator() override; - bool create(const QString &path, int width, int height, QImage &img) override; + bool createV3(const QString &path, int width, int height, QImage &img, qreal devicePixelRatio) override; Flags flags() const override; private: diff --git a/thumbnail/textcreator.cpp b/thumbnail/textcreator.cpp --- a/thumbnail/textcreator.cpp +++ b/thumbnail/textcreator.cpp @@ -34,6 +34,11 @@ #include #include +#include +#if LIBMAGIC_FOUND + #include "magic.h" +#endif + // TODO Fix or remove kencodingprober code // #include @@ -56,17 +61,52 @@ delete [] m_data; } -static QTextCodec *codecFromContent(const char *data, int dataSize) +#if LIBMAGIC_FOUND +static QTextCodec* codecFromFile(const QString &path) +{ + magic_t m = magic_open(MAGIC_MIME_ENCODING); + magic_load(m, nullptr); + const char *ret = magic_file(m, path.toLocal8Bit() ); + auto codecName = QString(ret).toUpper().toLocal8Bit(); + magic_close(m); + + if (QTextCodec::availableCodecs().contains(codecName)) { + return QTextCodec::codecForName(codecName); + } + + if (strcmp(ret, "unknown-8bit")) { + // use latin for unkwnwn 8bit as it is quite versatile + return QTextCodec::codecForName("latin-1"); + } + + return nullptr; +} +#endif + +static QTextCodec *codecFromContent(const char *data, int dataSize, const QString &path) { -#if 0 // ### Use this when KEncodingProber does not return junk encoding for UTF-8 data) - KEncodingProber prober; - prober.feed(data, dataSize); - return QTextCodec::codecForName(prober.encoding()); -#else QByteArray ba = QByteArray::fromRawData(data, dataSize); // try to detect UTF text, fall back to locale default (which is usually UTF-8) - return QTextCodec::codecForUtfText(ba, QTextCodec::codecForLocale()); + const auto codec = QTextCodec::codecForUtfText(ba, nullptr); + + if (codec == nullptr) { + // UTF-8 BOM detection failed + auto localCodec = QTextCodec::codecForLocale(); +#if LIBMAGIC_FOUND + if (localCodec->name() == "UTF-8") { + // UTF-8 was already tested in QTextCodec::codecForUtfText + // but only using bom presence + // The file could be utf-8 still or something entirely different + // use libmagic heuristics + auto codecMagic = codecFromFile(path); + if (codecMagic != nullptr) { + return codecMagic; + } + } #endif + return localCodec; + } + return codec; } bool TextCreator::create(const QString &path, int width, int height, QImage &img) @@ -126,7 +166,7 @@ { ok = true; m_data[read] = '\0'; - QString text = codecFromContent( m_data, read )->toUnicode( m_data, read ).trimmed(); + QString text = codecFromContent(m_data, read, path)->toUnicode(m_data, read).trimmed(); // FIXME: maybe strip whitespace and read more? // If the text contains tabs or consecutive spaces, it is probably diff --git a/thumbnail/thumbnail.h b/thumbnail/thumbnail.h --- a/thumbnail/thumbnail.h +++ b/thumbnail/thumbnail.h @@ -83,6 +83,7 @@ QSet m_propagationDirectories; QString m_thumbBasePath; qint64 m_maxFileSize; + qreal m_devicePixelRatio; }; #endif diff --git a/thumbnail/thumbnail.cpp b/thumbnail/thumbnail.cpp --- a/thumbnail/thumbnail.cpp +++ b/thumbnail/thumbnail.cpp @@ -140,6 +140,15 @@ return 0; } +bool createThumbnail(ThumbCreator *creator, const QString &path, int width, int height, QImage &img, qreal devicePixelRatio = 1.0) +{ + const auto thumbv3 = dynamic_cast(creator); + if (thumbv3) { + return thumbv3->createV3(path, width, height, img, devicePixelRatio); + } else { + return creator->create(path, width, height, img); + } +} ThumbnailProtocol::ThumbnailProtocol(const QByteArray &pool, const QByteArray &app) : SlaveBase("thumbnail", pool, app), @@ -201,6 +210,7 @@ m_width = metaData("width").toInt(); m_height = metaData("height").toInt(); int iconSize = metaData("iconSize").toInt(); + m_devicePixelRatio = metaData("devicePixelRatio").toInt(); if (m_width < 0 || m_height < 0) { error(KIO::ERR_INTERNAL, i18n("No or invalid size specified.")); @@ -263,14 +273,15 @@ if(sequenceCreator) sequenceCreator->setSequenceIndex(sequenceIndex()); - if (!creator->create(url.path(), m_width, m_height, img)) { + if (!createThumbnail(creator, url.path(), m_width, m_height, img, m_devicePixelRatio)) { error(KIO::ERR_INTERNAL, i18n("Cannot create thumbnail for %1", url.path())); return; } flags = creator->flags(); } } + qreal imgDpr = img.devicePixelRatioF(); scaleDownImage(img, m_width, m_height); if (flags & ThumbCreator::DrawFrame) { @@ -342,21 +353,18 @@ error(KIO::ERR_INTERNAL, i18n("Failed to attach to shared memory segment %1", shmid)); return; } - if (img.width() * img.height() > m_width * m_height) { + if (img.width() / m_devicePixelRatio * img.height() / m_devicePixelRatio > m_width * m_height) { error(KIO::ERR_INTERNAL, i18n("Image is too big for the shared memory segment")); shmdt((char*)shmaddr); return; } if( img.format() != QImage::Format_ARGB32 ) { // KIO::PreviewJob and this code below completely ignores colortable :-/, img = img.convertToFormat(QImage::Format_ARGB32); // so make sure there is none } - // Keep in sync with kdelibs/kio/kio/previewjob.cpp - stream << img.width() << img.height() << quint8(img.format()); -#if (QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)) + // Keep in sync with kio/src/previewjob.cpp + quint8 format = img.format() | 0x80; + stream << img.width() << img.height() << format << imgDpr; memcpy(shmaddr, img.bits(), img.sizeInBytes()); -#else - memcpy(shmaddr, img.bits(), img.byteCount()); -#endif shmdt((char*)shmaddr); mimeType("application/octet-stream"); data(imgData); @@ -409,25 +417,23 @@ // Scale the image down so it matches the aspect ratio float scaling = 1.0; - if ((image.size().width() > imageTargetSize.width()) && (imageTargetSize.width() != 0)) { - scaling = float(imageTargetSize.width()) / float(image.size().width()); + if ((image.width() > imageTargetSize.width() * image.devicePixelRatio()) && (imageTargetSize.width() != 0)) { + scaling = float(imageTargetSize.width()) / float(image.width()); } - QImage frame(imageTargetSize + QSize(frameWidth * 2, frameWidth * 2), - QImage::Format_ARGB32); - frame.fill(0); - - float scaledFrameWidth = frameWidth / scaling; + float scaledFrameWidth = frameWidth / scaling / image.devicePixelRatio(); QTransform m; m.rotate(qrand() % 17 - 8); // Random rotation ±8° m.scale(scaling, scaling); - QRectF frameRect(QPointF(0, 0), QPointF(image.width() + scaledFrameWidth*2, image.height() + scaledFrameWidth*2)); + QRectF frameRect(QPointF(0, 0), QPointF(image.width() / image.devicePixelRatio() + scaledFrameWidth * 2, + image.height() / image.devicePixelRatio() + scaledFrameWidth * 2)); QRect r = m.mapRect(QRectF(frameRect)).toAlignedRect(); - QImage transformed(r.size(), QImage::Format_ARGB32); + QImage transformed(r.size() * image.devicePixelRatio(), QImage::Format_ARGB32); + transformed.setDevicePixelRatio(image.devicePixelRatio()); transformed.fill(0); QPainter p(&transformed); p.setRenderHint(QPainter::SmoothPixmapTransform); @@ -484,12 +490,13 @@ QString localFile = directory.path(); KFileItem item(QUrl::fromLocalFile(localFile)); - const int extent = qMin(m_width, m_height); + const int extent = qMin(m_width * m_devicePixelRatio, m_height * m_devicePixelRatio); QPixmap folder = QIcon::fromTheme(item.iconName()).pixmap(extent); + folder.setDevicePixelRatio(m_devicePixelRatio); // Scale up base icon to ensure overlays are rendered with // the best quality possible even for low-res custom folder icons - if (qMax(folder.width(), folder.height()) < extent) { + if (qMax(folder.width() * m_devicePixelRatio, folder.height() * m_devicePixelRatio) < extent) { folder = folder.scaled(extent, extent, Qt::KeepAspectRatio, Qt::SmoothTransformation); } @@ -501,7 +508,7 @@ const int leftMargin = folderWidth / 13; const int rightMargin = leftMargin; - const int segmentWidth = (folderWidth - leftMargin - rightMargin + spacing) / tiles - spacing; + const int segmentWidth = (folderWidth - leftMargin - rightMargin + spacing) / tiles - spacing; const int segmentHeight = (folderHeight - topMargin - bottomMargin + spacing) / tiles - spacing; if ((segmentWidth < 5) || (segmentHeight < 5)) { // the segment size is too small for a useful preview @@ -512,6 +519,7 @@ int skipValidItems = ((int)sequenceIndex()) * tiles * tiles; img = QImage(QSize(folderWidth, folderHeight), QImage::Format_ARGB32); + img.setDevicePixelRatio(m_devicePixelRatio); img.fill(0); QPainter p; @@ -559,7 +567,8 @@ continue; } - if (!drawSubThumbnail(p, dir.filePath(), segmentWidth, segmentHeight, xPos, yPos, frameWidth)) { + if (!drawSubThumbnail(p, dir.filePath(), segmentWidth, segmentHeight + , xPos / m_devicePixelRatio, yPos / m_devicePixelRatio, frameWidth)) { continue; } @@ -621,6 +630,7 @@ // If only for one file a thumbnail could be generated then paint an image with only one tile if (validThumbnails == 1) { QImage oneTileImg(folder.size(), QImage::Format_ARGB32); + oneTileImg.setDevicePixelRatio(m_devicePixelRatio); oneTileImg.fill(0); QPainter oneTilePainter(&oneTileImg); @@ -631,7 +641,7 @@ const int oneTileWidth = folderWidth - leftMargin - rightMargin; const int oneTileHeight = folderHeight - topMargin - bottomMargin; - drawSubThumbnail(oneTilePainter, hadFirstThumbnail, oneTileWidth, oneTileHeight, leftMargin, topMargin, frameWidth); + drawSubThumbnail(oneTilePainter, hadFirstThumbnail, oneTileWidth, oneTileHeight, leftMargin / m_devicePixelRatio, topMargin / m_devicePixelRatio, frameWidth); return oneTileImg; } @@ -696,7 +706,7 @@ if ((segmentWidth <= 256) && (segmentHeight <= 256)) { // check whether a cached version of the file is available for - // 128 x 128 or 256 x 256 pixels + // 128 x 128 or 256 x 256 pixels (the saved image can have a scaling factor applied) int cacheSize = 0; QCryptographicHash md5(QCryptographicHash::Md5); md5.addData(QFile::encodeName(fileUrl.toString())); @@ -722,34 +732,42 @@ if (!thumbnail.load(thumbPath.absoluteFilePath(thumbName))) { // no cached version is available, a new thumbnail must be created - QSaveFile thumbnailfile(thumbPath.absoluteFilePath(thumbName)); - bool savedCorrectly = false; - if (subCreator->create(filePath, cacheSize, cacheSize, thumbnail)) { + if (createThumbnail(subCreator, filePath, cacheSize, cacheSize, thumbnail, m_devicePixelRatio)) { + // Some thumnail creator do not respect the width height parameters scaleDownImage(thumbnail, cacheSize, cacheSize); // The thumbnail has been created successfully. Store the thumbnail - // to the cache for future access. + // to the cache for future access, with the scaling factor applied + QSaveFile thumbnailfile(thumbPath.absoluteFilePath(thumbName)); if (thumbnailfile.open(QIODevice::WriteOnly | QIODevice::Truncate)) { - savedCorrectly = thumbnail.save(&thumbnailfile, "PNG"); + if (thumbnail.save(&thumbnailfile, "PNG")) { + thumbnailfile.commit(); + } } } else { return false; } - if(savedCorrectly) - { - thumbnailfile.commit(); + } else { + if (thumbnail.width() > cacheSize || thumbnail.height() > cacheSize) { + // the cached thumbnail uses a scaling factor, let's find it + const auto maxDim = qMax(thumbnail.width(), thumbnail.height()); + const auto imgDpr = maxDim / cacheSize; + thumbnail.setDevicePixelRatio(imgDpr); } } - } else if (!subCreator->create(filePath, segmentWidth, segmentHeight, thumbnail)) { + } else if (!createThumbnail(subCreator, filePath, segmentWidth / m_devicePixelRatio, segmentHeight / m_devicePixelRatio, thumbnail, m_devicePixelRatio)) { return false; } + // Make sure the image fits in the segments + // Some thumbnail creators do not respect the width / height parameters + scaleDownImage(thumbnail, segmentWidth / m_devicePixelRatio, segmentHeight / m_devicePixelRatio); return true; } void ThumbnailProtocol::scaleDownImage(QImage& img, int maxWidth, int maxHeight) { - if (img.width() > maxWidth || img.height() > maxHeight) { - img = img.scaled(maxWidth, maxHeight, Qt::KeepAspectRatio, Qt::SmoothTransformation); + if (img.width() / img.devicePixelRatio() > maxWidth || img.height() / img.devicePixelRatio() > maxHeight) { + img = img.scaled(maxWidth * img.devicePixelRatio(), maxHeight * img.devicePixelRatio(), Qt::KeepAspectRatio, Qt::SmoothTransformation); } } @@ -759,22 +777,20 @@ if (!createSubThumbnail(subThumbnail, filePath, width, height)) { return false; } + scaleDownImage(subThumbnail, width, height); // Seed the random number generator so that it always returns the same result // for the same directory and sequence-item qsrand(qHash(filePath)); // Apply fake smooth scaling, as seen on several blogs - if (subThumbnail.width() > width * 4 || subThumbnail.height() > height * 4) { - subThumbnail = subThumbnail.scaled(width*4, height*4, Qt::KeepAspectRatio, Qt::FastTransformation); + if (subThumbnail.width() > width * 4 * subThumbnail.devicePixelRatio() || subThumbnail.height() > height * 4 * subThumbnail.devicePixelRatio()) { + subThumbnail = subThumbnail.scaled(width * 4 * subThumbnail.devicePixelRatio(), height * 4 * subThumbnail.devicePixelRatio(), Qt::KeepAspectRatio, Qt::FastTransformation); } - QSize targetSize(subThumbnail.size()); - targetSize.scale(width, height, Qt::KeepAspectRatio); - // center the image inside the segment boundaries - const QPoint centerPos(xPos + (width/ 2), yPos + (height / 2)); - drawPictureFrame(&p, centerPos, subThumbnail, frameWidth, targetSize); + const QPoint centerPos(xPos + (width/ 2 / m_devicePixelRatio), yPos + (height / 2 / m_devicePixelRatio)); + drawPictureFrame(&p, centerPos, subThumbnail, frameWidth, QSize(width, height)); return true; }