Start: Refactor f3d to use worker threads

This commit is contained in:
Chris Hennes
2025-02-08 14:14:56 -06:00
committed by Benjamin Nauck
parent da35868b96
commit 42cd4d76f2
17 changed files with 727 additions and 201 deletions

View File

@@ -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})

View File

@@ -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());
}
}
}

View File

@@ -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

View 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();
}

View 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

View File

@@ -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_

View 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);
}

View 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

View File

@@ -52,7 +52,7 @@ fc_target_copy_resource(StartScripts
${CMAKE_BINARY_DIR}/Mod/Start
${Start_Scripts})
INSTALL(
install(
FILES
${Start_Scripts}
DESTINATION

View File

@@ -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());