Start: Refactor f3d to use worker threads
This commit is contained in:
committed by
Benjamin Nauck
parent
da35868b96
commit
42cd4d76f2
@@ -25,7 +25,7 @@ set(Start_LIBS
|
||||
FreeCADApp
|
||||
)
|
||||
|
||||
SET(Start_SRCS
|
||||
set(Start_SRCS
|
||||
AppStart.cpp
|
||||
DisplayedFilesModel.cpp
|
||||
DisplayedFilesModel.h
|
||||
@@ -33,10 +33,14 @@ SET(Start_SRCS
|
||||
ExamplesModel.h
|
||||
CustomFolderModel.cpp
|
||||
CustomFolderModel.h
|
||||
FileUtilities.cpp
|
||||
FileUtilities.h
|
||||
PreCompiled.cpp
|
||||
PreCompiled.h
|
||||
RecentFilesModel.cpp
|
||||
RecentFilesModel.h)
|
||||
RecentFilesModel.h
|
||||
ThumbnailSource.cpp
|
||||
ThumbnailSource.h)
|
||||
|
||||
add_library(Start SHARED ${Start_SRCS})
|
||||
target_link_libraries(Start ${Start_LIBS})
|
||||
@@ -44,7 +48,7 @@ if (FREECAD_WARN_ERROR)
|
||||
target_compile_warn_error(Start)
|
||||
endif()
|
||||
|
||||
SET_BIN_DIR(Start Start /Mod/Start)
|
||||
SET_PYTHON_PREFIX_SUFFIX(Start)
|
||||
set_bin_dir(Start Start /Mod/Start)
|
||||
set_python_prefix_suffix(Start)
|
||||
|
||||
INSTALL(TARGETS Start DESTINATION ${CMAKE_INSTALL_LIBDIR})
|
||||
install(TARGETS Start DESTINATION ${CMAKE_INSTALL_LIBDIR})
|
||||
|
||||
@@ -25,55 +25,28 @@
|
||||
#ifndef _PreComp_
|
||||
#include <boost/algorithm/string/predicate.hpp>
|
||||
#include <QByteArray>
|
||||
#include <QCryptographicHash>
|
||||
#include <QDateTime>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QProcess>
|
||||
#include <QStandardPaths>
|
||||
#include <QTimeZone>
|
||||
#include <QThreadPool>
|
||||
#include <QUrl>
|
||||
#endif
|
||||
|
||||
#include "DisplayedFilesModel.h"
|
||||
|
||||
|
||||
#include "FileUtilities.h"
|
||||
#include "ThumbnailSource.h"
|
||||
#include <App/Application.h>
|
||||
#include <App/ProjectFile.h>
|
||||
#include <Base/FileInfo.h>
|
||||
#include <Base/Stream.h>
|
||||
#include <Base/TimeInfo.h>
|
||||
#include <Base/Stream.h>
|
||||
|
||||
|
||||
using namespace Start;
|
||||
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
std::string humanReadableSize(unsigned int bytes)
|
||||
{
|
||||
static const std::vector<std::string> siPrefix {
|
||||
"b",
|
||||
"kb",
|
||||
"Mb",
|
||||
"Gb",
|
||||
"Tb",
|
||||
"Pb",
|
||||
"Eb" // I think it's safe to stop here (for the time being)...
|
||||
};
|
||||
size_t base = 0;
|
||||
double inUnits = bytes;
|
||||
constexpr double siFactor {1000.0};
|
||||
while (inUnits > siFactor && base < siPrefix.size() - 1) {
|
||||
++base;
|
||||
inUnits /= siFactor;
|
||||
}
|
||||
if (base == 0) {
|
||||
// Don't include a decimal point for bytes
|
||||
return fmt::format("{:.0f} {}", inUnits, siPrefix[base]);
|
||||
}
|
||||
// For all others, include one digit after the decimal place
|
||||
return fmt::format("{:.1f} {}", inUnits, siPrefix[base]);
|
||||
}
|
||||
|
||||
FileStats fileInfoFromFreeCADFile(const std::string& path)
|
||||
{
|
||||
App::ProjectFile proj(path);
|
||||
@@ -90,146 +63,46 @@ FileStats fileInfoFromFreeCADFile(const std::string& path)
|
||||
return result;
|
||||
}
|
||||
|
||||
std::string getThumbnailsImage()
|
||||
{
|
||||
return "thumbnails/Thumbnail.png";
|
||||
}
|
||||
|
||||
QString getThumbnailsName()
|
||||
{
|
||||
#if defined(Q_OS_LINUX)
|
||||
return QStringLiteral("thumbnails/normal");
|
||||
#else
|
||||
return QStringLiteral("FreeCADStartThumbnails");
|
||||
#endif
|
||||
}
|
||||
|
||||
QDir getThumnailsParentDir()
|
||||
{
|
||||
return {QStandardPaths::writableLocation(QStandardPaths::GenericCacheLocation)};
|
||||
}
|
||||
|
||||
QString getThumbnailsDir()
|
||||
{
|
||||
QDir dir = getThumnailsParentDir();
|
||||
return dir.absoluteFilePath(getThumbnailsName());
|
||||
}
|
||||
|
||||
void createThumbnailsDir()
|
||||
{
|
||||
QString name = getThumbnailsName();
|
||||
QDir dir(getThumnailsParentDir());
|
||||
if (!dir.exists(name)) {
|
||||
dir.mkpath(name);
|
||||
}
|
||||
}
|
||||
|
||||
QString getMD5Hash(const std::string& path)
|
||||
{
|
||||
// Use MD5 hash as specified here:
|
||||
// https://specifications.freedesktop.org/thumbnail-spec/0.8.0/thumbsave.html
|
||||
QUrl url(QString::fromStdString(path));
|
||||
url.setScheme(QStringLiteral("file"));
|
||||
QCryptographicHash hash(QCryptographicHash::Md5);
|
||||
hash.addData(url.toEncoded());
|
||||
QByteArray ba = hash.result().toHex();
|
||||
return QString::fromLatin1(ba);
|
||||
}
|
||||
|
||||
QString getUniquePNG(const std::string& path)
|
||||
{
|
||||
QDir dir = getThumbnailsDir();
|
||||
QString md5 = getMD5Hash(path) + QLatin1String(".png");
|
||||
return dir.absoluteFilePath(md5);
|
||||
}
|
||||
|
||||
bool useCachedPNG(const std::string& image, const std::string& project)
|
||||
{
|
||||
Base::FileInfo f1(image);
|
||||
Base::FileInfo f2(project);
|
||||
if (!f1.exists()) {
|
||||
return false;
|
||||
}
|
||||
if (!f2.exists()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return f1.lastModified() > f2.lastModified();
|
||||
}
|
||||
|
||||
/// Load the thumbnail image data (if any) that is stored in an FCStd file.
|
||||
/// \returns The image bytes, or an empty QByteArray (if no thumbnail was stored)
|
||||
QByteArray loadFCStdThumbnail(const std::string& pathToFCStdFile)
|
||||
QByteArray loadFCStdThumbnail(const QString& pathToFCStdFile)
|
||||
{
|
||||
App::ProjectFile proj(pathToFCStdFile);
|
||||
if (proj.loadDocument()) {
|
||||
if (App::ProjectFile proj(pathToFCStdFile.toStdString()); proj.loadDocument()) {
|
||||
try {
|
||||
std::string thumbnailFile = getUniquePNG(pathToFCStdFile).toStdString();
|
||||
if (!useCachedPNG(thumbnailFile, pathToFCStdFile)) {
|
||||
static std::string thumb = getThumbnailsImage();
|
||||
if (proj.containsFile(thumb)) {
|
||||
const QString pathToCachedThumbnail = getPathToCachedThumbnail(pathToFCStdFile);
|
||||
if (!useCachedThumbnail(pathToCachedThumbnail, pathToFCStdFile)) {
|
||||
static const QString pathToThumbnail = defaultThumbnailPath;
|
||||
if (proj.containsFile(pathToThumbnail.toStdString())) {
|
||||
createThumbnailsDir();
|
||||
Base::FileInfo fi(thumbnailFile);
|
||||
Base::ofstream str(fi, std::ios::out | std::ios::binary);
|
||||
proj.readInputFileDirect(thumb, str);
|
||||
str.close();
|
||||
const Base::FileInfo fi(pathToCachedThumbnail.toStdString());
|
||||
Base::ofstream stream(fi, std::ios::out | std::ios::binary);
|
||||
proj.readInputFileDirect(pathToThumbnail.toStdString(), stream);
|
||||
stream.close();
|
||||
}
|
||||
}
|
||||
|
||||
auto inputFile = QFile(QString::fromStdString(thumbnailFile));
|
||||
if (inputFile.exists()) {
|
||||
if (auto inputFile = QFile(pathToCachedThumbnail); inputFile.exists()) {
|
||||
inputFile.open(QIODevice::OpenModeFlag::ReadOnly);
|
||||
return inputFile.readAll();
|
||||
}
|
||||
}
|
||||
catch (...) {
|
||||
Base::Console().Log("Failed to load thumbnail for %s\n", pathToFCStdFile.toStdString());
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
/// Attempt to generate a thumbnail image from a file using f3d or load from cache
|
||||
/// \returns The image bytes, or an empty QByteArray (if thumbnail generation fails)
|
||||
QByteArray getF3dThumbnail(const std::string& pathToFile)
|
||||
{
|
||||
QString thumbnailPath = getUniquePNG(pathToFile);
|
||||
if (!useCachedPNG(thumbnailPath.toStdString(), pathToFile)) {
|
||||
ParameterGrp::handle hGrp = App::GetApplication().GetParameterGroupByPath(
|
||||
"User parameter:BaseApp/Preferences/Mod/Start");
|
||||
auto f3d = QString::fromUtf8(hGrp->GetASCII("f3d", "f3d").c_str());
|
||||
const int resolution = 128;
|
||||
QStringList args;
|
||||
args << QLatin1String("--config=thumbnail") << QLatin1String("--load-plugins=occt")
|
||||
<< QLatin1String("--verbose=quiet") << QLatin1String("--output=") + thumbnailPath
|
||||
<< QLatin1String("--resolution=") + QString::number(resolution) + QLatin1String(",")
|
||||
+ QString::number(resolution)
|
||||
<< QString::fromStdString(pathToFile);
|
||||
|
||||
QProcess process;
|
||||
process.start(f3d, args);
|
||||
process.waitForFinished();
|
||||
if (process.exitCode() != 0) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
QFile thumbnailFile(thumbnailPath);
|
||||
if (thumbnailFile.exists()) {
|
||||
thumbnailFile.open(QIODevice::OpenModeFlag::ReadOnly);
|
||||
return thumbnailFile.readAll();
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
FileStats getFileInfo(const std::string& path)
|
||||
{
|
||||
FileStats result;
|
||||
Base::FileInfo file(path);
|
||||
const Base::FileInfo file(path);
|
||||
if (file.hasExtension("FCStd")) {
|
||||
result = fileInfoFromFreeCADFile(path);
|
||||
}
|
||||
else {
|
||||
file.lastModified();
|
||||
result.insert(
|
||||
std::make_pair(DisplayedFilesModelRoles::modifiedTime, getLastModifiedAsString(file)));
|
||||
}
|
||||
result.insert(std::make_pair(DisplayedFilesModelRoles::path, path));
|
||||
result.insert(std::make_pair(DisplayedFilesModelRoles::size, humanReadableSize(file.size())));
|
||||
@@ -241,16 +114,13 @@ bool freecadCanOpen(const QString& extension)
|
||||
{
|
||||
std::string ext = extension.toStdString();
|
||||
auto importTypes = App::GetApplication().getImportTypes();
|
||||
return std::find_if(importTypes.begin(),
|
||||
importTypes.end(),
|
||||
[&ext](const auto& item) {
|
||||
return boost::iequals(item, ext);
|
||||
})
|
||||
return std::ranges::find_if(importTypes,
|
||||
[&ext](const auto& item) {
|
||||
return boost::iequals(item, ext);
|
||||
})
|
||||
!= importTypes.end();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
DisplayedFilesModel::DisplayedFilesModel(QObject* parent)
|
||||
: QAbstractListModel(parent)
|
||||
{}
|
||||
@@ -262,15 +132,14 @@ int DisplayedFilesModel::rowCount(const QModelIndex& parent) const
|
||||
return static_cast<int>(_fileInfoCache.size());
|
||||
}
|
||||
|
||||
QVariant DisplayedFilesModel::data(const QModelIndex& index, int roleAsInt) const
|
||||
QVariant DisplayedFilesModel::data(const QModelIndex& index, int role) const
|
||||
{
|
||||
int row = index.row();
|
||||
const int row = index.row();
|
||||
if (row < 0 || row >= static_cast<int>(_fileInfoCache.size())) {
|
||||
return {};
|
||||
}
|
||||
auto mapEntry = _fileInfoCache.at(row);
|
||||
auto role = static_cast<DisplayedFilesModelRoles>(roleAsInt);
|
||||
switch (role) {
|
||||
const auto mapEntry = _fileInfoCache.at(row);
|
||||
switch (const auto roleAsType = static_cast<DisplayedFilesModelRoles>(role)) {
|
||||
case DisplayedFilesModelRoles::author: // NOLINT(bugprone-branch-clone)
|
||||
[[fallthrough]];
|
||||
case DisplayedFilesModelRoles::baseName:
|
||||
@@ -288,15 +157,14 @@ QVariant DisplayedFilesModel::data(const QModelIndex& index, int roleAsInt) cons
|
||||
case DisplayedFilesModelRoles::path:
|
||||
[[fallthrough]];
|
||||
case DisplayedFilesModelRoles::size:
|
||||
if (mapEntry.find(role) != mapEntry.end()) {
|
||||
return QString::fromStdString(mapEntry.at(role));
|
||||
}
|
||||
else {
|
||||
return {};
|
||||
if (mapEntry.contains(roleAsType)) {
|
||||
return QString::fromStdString(mapEntry.at(roleAsType));
|
||||
}
|
||||
break;
|
||||
case DisplayedFilesModelRoles::image: {
|
||||
auto path = QString::fromStdString(mapEntry.at(DisplayedFilesModelRoles::path));
|
||||
if (_imageCache.contains(path)) {
|
||||
if (const auto path =
|
||||
QString::fromStdString(mapEntry.at(DisplayedFilesModelRoles::path));
|
||||
_imageCache.contains(path)) {
|
||||
return _imageCache[path];
|
||||
}
|
||||
break;
|
||||
@@ -304,16 +172,19 @@ QVariant DisplayedFilesModel::data(const QModelIndex& index, int roleAsInt) cons
|
||||
default:
|
||||
break;
|
||||
}
|
||||
switch (roleAsInt) {
|
||||
switch (role) {
|
||||
case Qt::ItemDataRole::ToolTipRole:
|
||||
return QString::fromStdString(mapEntry.at(DisplayedFilesModelRoles::path));
|
||||
default:
|
||||
// No other role gets handled
|
||||
break;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
void DisplayedFilesModel::addFile(const QString& filePath)
|
||||
{
|
||||
QFileInfo qfi(filePath);
|
||||
const QFileInfo qfi(filePath);
|
||||
if (!qfi.isReadable()) {
|
||||
return;
|
||||
}
|
||||
@@ -323,17 +194,28 @@ void DisplayedFilesModel::addFile(const QString& filePath)
|
||||
}
|
||||
|
||||
_fileInfoCache.emplace_back(getFileInfo(filePath.toStdString()));
|
||||
if (qfi.suffix().toLower() == QLatin1String("fcstd")) {
|
||||
auto thumbnail = loadFCStdThumbnail(filePath.toStdString());
|
||||
if (!thumbnail.isEmpty()) {
|
||||
const auto lowercaseExtension = qfi.suffix().toLower();
|
||||
const QStringList ignoredExtensions {QLatin1String("fcmacro"),
|
||||
QLatin1String("py"),
|
||||
QLatin1String("pyi"),
|
||||
QLatin1String("csv"),
|
||||
QLatin1String("txt")};
|
||||
if (lowercaseExtension == QLatin1String("fcstd")) {
|
||||
if (const auto thumbnail = loadFCStdThumbnail(filePath); !thumbnail.isEmpty()) {
|
||||
_imageCache.insert(filePath, thumbnail);
|
||||
}
|
||||
}
|
||||
else if (ignoredExtensions.contains(lowercaseExtension)) {
|
||||
// Don't try to generate a thumbnail for things like this: FreeCAD can read them, but
|
||||
// there's not much point in showing anything besides a generic icon
|
||||
}
|
||||
else {
|
||||
auto thumbnail = getF3dThumbnail(filePath.toStdString());
|
||||
if (!thumbnail.isEmpty()) {
|
||||
_imageCache.insert(filePath, thumbnail);
|
||||
}
|
||||
const auto runner = new ThumbnailSource(filePath);
|
||||
connect(runner->signals(),
|
||||
&ThumbnailSourceSignals::thumbnailAvailable,
|
||||
this,
|
||||
&DisplayedFilesModel::processNewThumbnail);
|
||||
QThreadPool::globalInstance()->start(runner);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -345,16 +227,37 @@ void DisplayedFilesModel::clear()
|
||||
QHash<int, QByteArray> DisplayedFilesModel::roleNames() const
|
||||
{
|
||||
static QHash<int, QByteArray> nameMap {
|
||||
std::make_pair(int(DisplayedFilesModelRoles::author), "author"),
|
||||
std::make_pair(int(DisplayedFilesModelRoles::baseName), "baseName"),
|
||||
std::make_pair(int(DisplayedFilesModelRoles::company), "company"),
|
||||
std::make_pair(int(DisplayedFilesModelRoles::creationTime), "creationTime"),
|
||||
std::make_pair(int(DisplayedFilesModelRoles::description), "description"),
|
||||
std::make_pair(int(DisplayedFilesModelRoles::image), "image"),
|
||||
std::make_pair(int(DisplayedFilesModelRoles::license), "license"),
|
||||
std::make_pair(int(DisplayedFilesModelRoles::modifiedTime), "modifiedTime"),
|
||||
std::make_pair(int(DisplayedFilesModelRoles::path), "path"),
|
||||
std::make_pair(int(DisplayedFilesModelRoles::size), "size"),
|
||||
std::make_pair(static_cast<int>(DisplayedFilesModelRoles::author), "author"),
|
||||
std::make_pair(static_cast<int>(DisplayedFilesModelRoles::baseName), "baseName"),
|
||||
std::make_pair(static_cast<int>(DisplayedFilesModelRoles::company), "company"),
|
||||
std::make_pair(static_cast<int>(DisplayedFilesModelRoles::creationTime), "creationTime"),
|
||||
std::make_pair(static_cast<int>(DisplayedFilesModelRoles::description), "description"),
|
||||
std::make_pair(static_cast<int>(DisplayedFilesModelRoles::image), "image"),
|
||||
std::make_pair(static_cast<int>(DisplayedFilesModelRoles::license), "license"),
|
||||
std::make_pair(static_cast<int>(DisplayedFilesModelRoles::modifiedTime), "modifiedTime"),
|
||||
std::make_pair(static_cast<int>(DisplayedFilesModelRoles::path), "path"),
|
||||
std::make_pair(static_cast<int>(DisplayedFilesModelRoles::size), "size"),
|
||||
};
|
||||
return nameMap;
|
||||
}
|
||||
|
||||
void DisplayedFilesModel::processNewThumbnail(const QString& file, const QByteArray& thumbnail)
|
||||
{
|
||||
if (!thumbnail.isEmpty()) {
|
||||
_imageCache.insert(file, thumbnail);
|
||||
|
||||
// Figure out the index of this file...
|
||||
auto it = std::ranges::find_if(_fileInfoCache, [file](const FileStats& row) {
|
||||
auto pathIt = row.find(DisplayedFilesModelRoles::path);
|
||||
return pathIt != row.end() && pathIt->second == file.toStdString();
|
||||
});
|
||||
if (it != _fileInfoCache.end()) {
|
||||
std::size_t index = std::distance(_fileInfoCache.begin(), it);
|
||||
QModelIndex qmi = createIndex(index, 0);
|
||||
Q_EMIT(dataChanged(qmi, qmi, {static_cast<int>(DisplayedFilesModelRoles::image)}));
|
||||
}
|
||||
else {
|
||||
Base::Console().Log("Unrecognized path %s\n", file.toStdString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,8 +21,8 @@
|
||||
* *
|
||||
***************************************************************************/
|
||||
|
||||
#ifndef FREECAD_START_DISPLAYEDFILESMODEL_H
|
||||
#define FREECAD_START_DISPLAYEDFILESMODEL_H
|
||||
#ifndef FREECAD_START_DISPLAYED_FILES_MODEL_H
|
||||
#define FREECAD_START_DISPLAYED_FILES_MODEL_H
|
||||
|
||||
#include <QAbstractListModel>
|
||||
#include <Base/Parameter.h>
|
||||
@@ -69,10 +69,8 @@ protected:
|
||||
/// DisplayedFilesModelRoles enumeration
|
||||
QHash<int, QByteArray> roleNames() const override;
|
||||
|
||||
/// Destroy and recreate the cache of info about the files. Should be connected to a signal
|
||||
/// indicating when some piece of information about the files has changed. Does NOT generate
|
||||
/// a new list of files, only re-caches the existing ones.
|
||||
void reCacheFileInfo();
|
||||
/// Process a new thumbnail produces by some sort of worker thread
|
||||
void processNewThumbnail(const QString& file, const QByteArray& thumbnail);
|
||||
|
||||
private:
|
||||
std::vector<FileStats> _fileInfoCache;
|
||||
@@ -81,4 +79,4 @@ private:
|
||||
|
||||
} // namespace Start
|
||||
|
||||
#endif // FREECAD_START_DISPLAYEDFILESMODEL_H
|
||||
#endif // FREECAD_START_DISPLAYED_FILES_MODEL_H
|
||||
|
||||
109
src/Mod/Start/App/FileUtilities.cpp
Normal file
109
src/Mod/Start/App/FileUtilities.cpp
Normal file
@@ -0,0 +1,109 @@
|
||||
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
/****************************************************************************
|
||||
* *
|
||||
* Copyright (c) 2025 The FreeCAD Project Association AISBL *
|
||||
* *
|
||||
* This file is part of FreeCAD. *
|
||||
* *
|
||||
* FreeCAD is free software: you can redistribute it and/or modify it *
|
||||
* under the terms of the GNU Lesser General Public License as *
|
||||
* published by the Free Software Foundation, either version 2.1 of the *
|
||||
* License, or (at your option) any later version. *
|
||||
* *
|
||||
* FreeCAD 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. *
|
||||
* *
|
||||
* You should have received a copy of the GNU Lesser General Public *
|
||||
* License along with FreeCAD. If not, see *
|
||||
* <https://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
***************************************************************************/
|
||||
|
||||
#include "PreCompiled.h"
|
||||
#ifndef _PreComp_
|
||||
#include <QCryptographicHash>
|
||||
#include <QDateTime>
|
||||
#include <QFileInfo>
|
||||
#include <QString>
|
||||
#include <QTimeZone>
|
||||
#include <QUrl>
|
||||
#include <fmt/format.h>
|
||||
#endif
|
||||
|
||||
#include "FileUtilities.h"
|
||||
#include <Base/TimeInfo.h>
|
||||
|
||||
|
||||
void Start::createThumbnailsDir()
|
||||
{
|
||||
if (!thumbnailsParentDir.exists(defaultThumbnailName)) {
|
||||
thumbnailsParentDir.mkpath(defaultThumbnailName);
|
||||
}
|
||||
}
|
||||
|
||||
QString Start::getMD5Hash(const QString& path)
|
||||
{
|
||||
// Use MD5 hash as specified here:
|
||||
// https://specifications.freedesktop.org/thumbnail-spec/0.8.0/thumbsave.html
|
||||
QUrl url(path);
|
||||
url.setScheme(QStringLiteral("file"));
|
||||
QCryptographicHash hash(QCryptographicHash::Md5);
|
||||
hash.addData(url.toEncoded());
|
||||
const QByteArray ba = hash.result().toHex();
|
||||
return QString::fromLatin1(ba);
|
||||
}
|
||||
|
||||
QString Start::getPathToCachedThumbnail(const QString& path)
|
||||
{
|
||||
const QString md5 = getMD5Hash(path) + QLatin1String(".png");
|
||||
return thumbnailsDir.absoluteFilePath(md5);
|
||||
}
|
||||
|
||||
bool Start::useCachedThumbnail(const QString& image, const QString& project)
|
||||
{
|
||||
const QFileInfo f1(image);
|
||||
const QFileInfo f2(project);
|
||||
if (!f1.exists()) {
|
||||
return false;
|
||||
}
|
||||
if (!f2.exists()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return f1.lastModified() > f2.lastModified();
|
||||
}
|
||||
|
||||
std::string Start::humanReadableSize(uint64_t bytes)
|
||||
{
|
||||
if (bytes == 0) {
|
||||
return "0 B";
|
||||
}
|
||||
|
||||
// uint64_t can't express numbers higher than the EB range (< 20 EB)
|
||||
constexpr std::array units {"B", "kB", "MB", "GB", "TB", "PB", "EB"};
|
||||
constexpr double unitFactor = 1000.0;
|
||||
|
||||
const double logBaseFactor = std::log10(unitFactor); // change to constexpr on c++26
|
||||
const auto unitIndex = static_cast<size_t>(std::log10(bytes) / logBaseFactor);
|
||||
|
||||
// unitIndex can't be out of range because uint64_t can't express numbers higher than the EB
|
||||
// range
|
||||
const auto unit = units.at(unitIndex);
|
||||
|
||||
const double scaledValue = static_cast<double>(bytes) / std::pow(unitFactor, unitIndex);
|
||||
|
||||
const bool isByteUnit = unitIndex == 0;
|
||||
const size_t precision = isByteUnit ? 0 : 1;
|
||||
return fmt::format("{:.{}f} {}", scaledValue, precision, unit);
|
||||
}
|
||||
|
||||
std::string Start::getLastModifiedAsString(const Base::FileInfo& file)
|
||||
{
|
||||
Base::TimeInfo lastModified = file.lastModified();
|
||||
return QDateTime::fromSecsSinceEpoch(lastModified.getTime_t())
|
||||
.toTimeZone(QTimeZone::utc())
|
||||
.toString(Qt::ISODate)
|
||||
.toStdString();
|
||||
}
|
||||
67
src/Mod/Start/App/FileUtilities.h
Normal file
67
src/Mod/Start/App/FileUtilities.h
Normal file
@@ -0,0 +1,67 @@
|
||||
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
/****************************************************************************
|
||||
* *
|
||||
* Copyright (c) 2025 The FreeCAD Project Association AISBL *
|
||||
* *
|
||||
* This file is part of FreeCAD. *
|
||||
* *
|
||||
* FreeCAD is free software: you can redistribute it and/or modify it *
|
||||
* under the terms of the GNU Lesser General Public License as *
|
||||
* published by the Free Software Foundation, either version 2.1 of the *
|
||||
* License, or (at your option) any later version. *
|
||||
* *
|
||||
* FreeCAD 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. *
|
||||
* *
|
||||
* You should have received a copy of the GNU Lesser General Public *
|
||||
* License along with FreeCAD. If not, see *
|
||||
* <https://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
***************************************************************************/
|
||||
|
||||
#ifndef FREECAD_FILEUTILITIES_H
|
||||
#define FREECAD_FILEUTILITIES_H
|
||||
|
||||
#include "Base/FileInfo.h"
|
||||
#include "Mod/Start/StartGlobal.h"
|
||||
|
||||
#include <qglobal.h>
|
||||
#include <QDir>
|
||||
#include <QStandardPaths>
|
||||
|
||||
class QString;
|
||||
|
||||
namespace Start
|
||||
{
|
||||
|
||||
const QLatin1String defaultThumbnailPath("thumbnails/Thumbnail.png");
|
||||
|
||||
const QLatin1String defaultThumbnailName
|
||||
#if defined(Q_OS_LINUX)
|
||||
("thumbnails/normal");
|
||||
#else
|
||||
("FreeCADStartThumbnails");
|
||||
#endif
|
||||
|
||||
const QDir thumbnailsParentDir {
|
||||
QStandardPaths::writableLocation(QStandardPaths::GenericCacheLocation)};
|
||||
|
||||
const QDir thumbnailsDir {thumbnailsParentDir.absoluteFilePath(defaultThumbnailName)};
|
||||
|
||||
StartExport void createThumbnailsDir();
|
||||
|
||||
StartExport QString getMD5Hash(const QString& path);
|
||||
|
||||
StartExport QString getPathToCachedThumbnail(const QString& path);
|
||||
|
||||
StartExport bool useCachedThumbnail(const QString& image, const QString& project);
|
||||
|
||||
StartExport std::string humanReadableSize(std::uint64_t bytes);
|
||||
|
||||
StartExport std::string getLastModifiedAsString(const Base::FileInfo& file);
|
||||
|
||||
} // namespace Start
|
||||
|
||||
#endif // FREECAD_FILEUTILITIES_H
|
||||
@@ -45,6 +45,9 @@
|
||||
// boost
|
||||
#include <boost/algorithm/string/predicate.hpp>
|
||||
|
||||
// fmt
|
||||
#include <fmt/format.h>
|
||||
|
||||
// Qt (should never include GUI files, only QtCore)
|
||||
#include <QByteArray>
|
||||
#include <QCryptographicHash>
|
||||
@@ -52,7 +55,15 @@
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QMetaObject>
|
||||
#include <QMutexLocker>
|
||||
#include <QObject>
|
||||
#include <QProcess>
|
||||
#include <QStandardPaths>
|
||||
#include <QString>
|
||||
#include <QThreadPool>
|
||||
#include <QTimeZone>
|
||||
#include <QTimer>
|
||||
#include <QUrl>
|
||||
|
||||
#endif // _PreComp_
|
||||
|
||||
190
src/Mod/Start/App/ThumbnailSource.cpp
Normal file
190
src/Mod/Start/App/ThumbnailSource.cpp
Normal file
@@ -0,0 +1,190 @@
|
||||
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
/****************************************************************************
|
||||
* *
|
||||
* Copyright (c) 2025 The FreeCAD Project Association AISBL *
|
||||
* *
|
||||
* This file is part of FreeCAD. *
|
||||
* *
|
||||
* FreeCAD is free software: you can redistribute it and/or modify it *
|
||||
* under the terms of the GNU Lesser General Public License as *
|
||||
* published by the Free Software Foundation, either version 2.1 of the *
|
||||
* License, or (at your option) any later version. *
|
||||
* *
|
||||
* FreeCAD 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. *
|
||||
* *
|
||||
* You should have received a copy of the GNU Lesser General Public *
|
||||
* License along with FreeCAD. If not, see *
|
||||
* <https://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
***************************************************************************/
|
||||
|
||||
#include "PreCompiled.h"
|
||||
#ifndef _PreComp_
|
||||
#include <QFile>
|
||||
#include <QMetaObject>
|
||||
#include <QMutexLocker>
|
||||
#include <QObject>
|
||||
#include <QProcess>
|
||||
#include <QTimer>
|
||||
#endif
|
||||
|
||||
#include "ThumbnailSource.h"
|
||||
|
||||
#include <QThread>
|
||||
|
||||
#include "FileUtilities.h"
|
||||
|
||||
#include <App/Application.h>
|
||||
|
||||
using namespace Start;
|
||||
|
||||
ThumbnailSource::F3DInstallation ThumbnailSource::_f3d {};
|
||||
QMutex ThumbnailSource::_mutex;
|
||||
|
||||
ThumbnailSource::ThumbnailSource(QString file)
|
||||
: _file(std::move(file))
|
||||
{}
|
||||
|
||||
ThumbnailSourceSignals* ThumbnailSource::signals()
|
||||
{
|
||||
return &_signals;
|
||||
}
|
||||
|
||||
void ThumbnailSource::run()
|
||||
{
|
||||
_thumbnailPath = getPathToCachedThumbnail(_file);
|
||||
if (!useCachedThumbnail(_thumbnailPath, _file)) {
|
||||
setupF3D(); // Go through the mutex to ensure data is not stale
|
||||
if (_f3d.major < 2) {
|
||||
return;
|
||||
}
|
||||
const ParameterGrp::handle hGrp = App::GetApplication().GetParameterGroupByPath(
|
||||
"User parameter:BaseApp/Preferences/Mod/Start");
|
||||
const auto f3d = QString::fromUtf8(hGrp->GetASCII("f3d", "f3d").c_str());
|
||||
QStringList args(_f3d.baseArgs);
|
||||
args << QLatin1String("--output=") + _thumbnailPath << _file;
|
||||
|
||||
QProcess process;
|
||||
Base::Console().Log("Creating thumbnail for %s...\n", _file.toStdString());
|
||||
process.start(f3d, args);
|
||||
if (!process.waitForFinished()) {
|
||||
process.kill();
|
||||
Base::Console().Log("Creating thumbnail for %s timed out\n", _file.toStdString());
|
||||
return;
|
||||
}
|
||||
if (process.exitStatus() == QProcess::CrashExit) {
|
||||
Base::Console().Log("Creating thumbnail for %s crashed\n", _file.toStdString());
|
||||
return;
|
||||
}
|
||||
if (process.exitCode() != 0) {
|
||||
Base::Console().Log("Creating thumbnail for %s failed\n", _file.toStdString());
|
||||
return;
|
||||
}
|
||||
Base::Console().Log("Creating thumbnail for %s succeeded, wrote to %s\n",
|
||||
_file.toStdString(),
|
||||
_thumbnailPath.toStdString());
|
||||
}
|
||||
if (QFile thumbnailFile(_thumbnailPath); thumbnailFile.exists()) {
|
||||
thumbnailFile.open(QIODevice::OpenModeFlag::ReadOnly);
|
||||
Q_EMIT _signals.thumbnailAvailable(_file, thumbnailFile.readAll());
|
||||
}
|
||||
}
|
||||
|
||||
std::tuple<int, int, int> extractF3DVersion(const QString& stdoutString)
|
||||
{
|
||||
int major {0};
|
||||
int minor {0};
|
||||
int patch {0};
|
||||
for (auto lines = stdoutString.split(QLatin1Char('\n')); const auto& line : lines) {
|
||||
if (line.startsWith(QLatin1String("Version: "))) {
|
||||
const auto substring = line.mid(8);
|
||||
if (auto split = substring.split(QLatin1Char('.')); split.size() >= 3) {
|
||||
try {
|
||||
major = split[0].toInt();
|
||||
minor = split[1].toInt();
|
||||
patch = split[2].toInt();
|
||||
}
|
||||
catch (...) {
|
||||
Base::Console().Log(
|
||||
"Could not determine F3D version, disabling thumbnail generation\n");
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
return std::make_tuple(major, minor, patch);
|
||||
}
|
||||
|
||||
QStringList getF3DOptions(const QString& f3d)
|
||||
{
|
||||
// F3D is under active development, and the available options change with some regularity.
|
||||
// Rather than hardcode per version, just check the ones we care about.
|
||||
QStringList optionsToTest {
|
||||
QStringLiteral("--load-plugins=occt"),
|
||||
QStringLiteral("--config=thumbnail"),
|
||||
QStringLiteral("--verbose=quiet"),
|
||||
QStringLiteral("--resolution=256,256"),
|
||||
QStringLiteral("--filename=0"),
|
||||
QStringLiteral("--grid=0"),
|
||||
QStringLiteral("--axis=0"),
|
||||
QStringLiteral("--no-background"),
|
||||
QStringLiteral("--max-size=100") // Max input file size in MB
|
||||
};
|
||||
QStringList goodOptions;
|
||||
for (const auto& option : optionsToTest) {
|
||||
QStringList args;
|
||||
args << option << QStringLiteral("--no-render");
|
||||
QProcess process;
|
||||
process.start(f3d, args);
|
||||
if (!process.waitForFinished()) {
|
||||
process.kill();
|
||||
continue;
|
||||
}
|
||||
auto stderrAsBytes = process.readAllStandardError();
|
||||
if (auto stderrAsString = QString::fromUtf8(stderrAsBytes);
|
||||
!stderrAsString.contains(QLatin1String("Unknown option"))) {
|
||||
goodOptions.append(option);
|
||||
}
|
||||
}
|
||||
return goodOptions;
|
||||
}
|
||||
|
||||
void ThumbnailSource::setupF3D()
|
||||
{
|
||||
QMutexLocker locker(&_mutex);
|
||||
if (_f3d.initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
// This method makes repeated blocking calls to f3d (both directly, the call below, and
|
||||
// indirectly, by calling getF3DOptions). By holding the mutex above, it ensures that these
|
||||
// calls complete before any process can attempt to make a real call to f3d to create thumbnail
|
||||
// data. ThumbnailSource is run in its own thread, so blocking here is appropriate and will not
|
||||
// affect any other part of the program.
|
||||
|
||||
_f3d.initialized = true; // Set immediately so we can use early-return below
|
||||
const ParameterGrp::handle hGrp = App::GetApplication().GetParameterGroupByPath(
|
||||
"User parameter:BaseApp/Preferences/Mod/Start");
|
||||
const auto f3d = QString::fromUtf8(hGrp->GetASCII("f3d", "f3d").c_str());
|
||||
const QStringList args {QLatin1String("--version")};
|
||||
QProcess process;
|
||||
process.start(f3d, args);
|
||||
if (!process.waitForFinished()) {
|
||||
process.kill();
|
||||
}
|
||||
if (process.exitCode() != 0) {
|
||||
return;
|
||||
}
|
||||
const QByteArray stdoutBytes = process.readAllStandardOutput();
|
||||
const auto stdoutString = QString::fromUtf8(stdoutBytes);
|
||||
const auto version = extractF3DVersion(stdoutString);
|
||||
_f3d.major = std::get<0>(version);
|
||||
_f3d.minor = std::get<1>(version);
|
||||
if (_f3d.major >= 2) {
|
||||
_f3d.baseArgs = getF3DOptions(f3d);
|
||||
}
|
||||
Base::Console().Log("Running f3d version %d.%d\n", _f3d.major, _f3d.minor);
|
||||
}
|
||||
85
src/Mod/Start/App/ThumbnailSource.h
Normal file
85
src/Mod/Start/App/ThumbnailSource.h
Normal file
@@ -0,0 +1,85 @@
|
||||
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
/****************************************************************************
|
||||
* *
|
||||
* Copyright (c) 2025 The FreeCAD Project Association AISBL *
|
||||
* *
|
||||
* This file is part of FreeCAD. *
|
||||
* *
|
||||
* FreeCAD is free software: you can redistribute it and/or modify it *
|
||||
* under the terms of the GNU Lesser General Public License as *
|
||||
* published by the Free Software Foundation, either version 2.1 of the *
|
||||
* License, or (at your option) any later version. *
|
||||
* *
|
||||
* FreeCAD 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. *
|
||||
* *
|
||||
* You should have received a copy of the GNU Lesser General Public *
|
||||
* License along with FreeCAD. If not, see *
|
||||
* <https://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
***************************************************************************/
|
||||
|
||||
#ifndef FREECAD_THUMBNAIL_SOURCE_H
|
||||
#define FREECAD_THUMBNAIL_SOURCE_H
|
||||
|
||||
#include <QMutex>
|
||||
#include <QObject>
|
||||
#include <QProcess>
|
||||
#include <QRunnable>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
|
||||
#include <memory>
|
||||
|
||||
namespace Start
|
||||
{
|
||||
|
||||
class ThumbnailSourceSignals: public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
Q_SIGNALS:
|
||||
|
||||
void thumbnailAvailable(const QString& file, const QByteArray& data);
|
||||
};
|
||||
|
||||
class ThumbnailSource: public QRunnable
|
||||
{
|
||||
public:
|
||||
explicit ThumbnailSource(QString file);
|
||||
|
||||
// Don't make copies of a ThumbnailSource (it's probably running a process, what would it mean
|
||||
// to copy it?):
|
||||
ThumbnailSource(ThumbnailSource&) = delete;
|
||||
ThumbnailSource(ThumbnailSource&&) = delete;
|
||||
ThumbnailSource operator=(const ThumbnailSource&) = delete;
|
||||
ThumbnailSource operator=(ThumbnailSource&&) = delete;
|
||||
|
||||
void run() override;
|
||||
|
||||
ThumbnailSourceSignals* signals();
|
||||
|
||||
private:
|
||||
static void setupF3D();
|
||||
|
||||
QString _file;
|
||||
QString _thumbnailPath;
|
||||
ThumbnailSourceSignals _signals;
|
||||
|
||||
/// Gather together all of the f3d information protected by the mutex: data in this struct
|
||||
/// should be accessed only after a call to setupF3D() to ensure synchronization.
|
||||
static struct F3DInstallation
|
||||
{
|
||||
bool initialized {false};
|
||||
int major {0};
|
||||
int minor {0};
|
||||
QStringList baseArgs;
|
||||
} _f3d;
|
||||
static QMutex _mutex;
|
||||
};
|
||||
|
||||
} // namespace Start
|
||||
|
||||
#endif // FREECAD_THUMBNAIL_SOURCE_H
|
||||
@@ -52,7 +52,7 @@ fc_target_copy_resource(StartScripts
|
||||
${CMAKE_BINARY_DIR}/Mod/Start
|
||||
${Start_Scripts})
|
||||
|
||||
INSTALL(
|
||||
install(
|
||||
FILES
|
||||
${Start_Scripts}
|
||||
DESTINATION
|
||||
|
||||
@@ -149,6 +149,8 @@ QPixmap FileCardDelegate::generateThumbnail(const QString& path) const
|
||||
auto thumbnailSize =
|
||||
static_cast<int>(_parameterGroup->GetInt("FileThumbnailIconsSize", 128)); // NOLINT
|
||||
if (path.endsWith(QLatin1String(".fcstd"), Qt::CaseSensitivity::CaseInsensitive)) {
|
||||
// This is a fallback, the model will have pulled the thumbnail out of the FCStd file if it
|
||||
// existed.
|
||||
QImageReader reader(QLatin1String(":/icons/freecad-doc.svg"));
|
||||
reader.setScaledSize({thumbnailSize, thumbnailSize});
|
||||
return QPixmap::fromImage(reader.read());
|
||||
|
||||
Reference in New Issue
Block a user