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 5635c41ed5
commit 71932ac724
17 changed files with 727 additions and 201 deletions

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