629 lines
18 KiB
C++
629 lines
18 KiB
C++
#include "scriptengine.h"
|
|
#include <QDir>
|
|
#include <QFileInfo>
|
|
#include <QDebug>
|
|
#include <QInputDialog>
|
|
#include "loadrunable.h"
|
|
#include "rawimage.h"
|
|
#include "loadrunable.h"
|
|
#include "batchprocessing.h"
|
|
#include <fitsio2.h>
|
|
#include "libXISF/libxisf.h"
|
|
|
|
namespace Script
|
|
{
|
|
|
|
ScriptEngine::ScriptEngine(BatchProcessing *parent)
|
|
: _jsEngine(new QJSEngine(this))
|
|
, _database(new Database(this))
|
|
, _parent(parent)
|
|
, _pool(new QThreadPool(this))
|
|
{
|
|
QJSValue core = _jsEngine->newQObject(this);
|
|
_jsEngine->globalObject().setProperty("core", core);
|
|
QJSValue fitsRecordObject = _jsEngine->newQMetaObject(&FITSRecordModify::staticMetaObject);
|
|
_jsEngine->globalObject().setProperty("FITSRecordModify", fitsRecordObject);
|
|
_database->init(QLatin1String("scriptengine"));
|
|
_semaphore.release(_pool->maxThreadCount());
|
|
}
|
|
|
|
void ScriptEngine::setParams(const QString &scriptPath, const QList<QPair<QString, QString>> &paths, const QString &outputDir)
|
|
{
|
|
_scriptPath = scriptPath;
|
|
_paths = paths;
|
|
_outputDir = outputDir + "/";
|
|
}
|
|
|
|
void ScriptEngine::reportError(const QString &message)
|
|
{
|
|
_jsEngine->throwError(message);
|
|
}
|
|
|
|
const QString &ScriptEngine::outputDir() const
|
|
{
|
|
return _outputDir;
|
|
}
|
|
|
|
void ScriptEngine::interrupt()
|
|
{
|
|
_jsEngine->setInterrupted(true);
|
|
}
|
|
|
|
void ScriptEngine::logError(const QString &message)
|
|
{
|
|
emit newMessage(message, true);
|
|
}
|
|
|
|
void ScriptEngine::log(const QString &message)
|
|
{
|
|
emit newMessage(message, false);
|
|
}
|
|
|
|
void ScriptEngine::mark(File *file)
|
|
{
|
|
_database->mark(file->absoluteFilePath());
|
|
}
|
|
|
|
void ScriptEngine::unmark(File *file)
|
|
{
|
|
_database->unmark(file->absoluteFilePath());
|
|
}
|
|
|
|
bool ScriptEngine::isMarked(const File *file) const
|
|
{
|
|
return _database->isMarked(file->absoluteFilePath());
|
|
}
|
|
|
|
void ScriptEngine::setMaxThread(int maxthread)
|
|
{
|
|
int newval = std::max(std::min(QThread::idealThreadCount(), maxthread), 1);
|
|
int oldval = _pool->maxThreadCount();
|
|
if(newval > oldval)
|
|
_semaphore.release(newval - oldval);
|
|
else if(newval < oldval)
|
|
_semaphore.acquire(oldval - newval);
|
|
_pool->setMaxThreadCount(newval);
|
|
}
|
|
|
|
void ScriptEngine::sync()
|
|
{
|
|
_pool->waitForDone();
|
|
}
|
|
|
|
QJSValue ScriptEngine::getString(const QString &label, const QString &text) const
|
|
{
|
|
QJSValue ret;
|
|
QMetaObject::invokeMethod(_parent, "getString", Qt::BlockingQueuedConnection, Q_RETURN_ARG(QJSValue, ret), Q_ARG(QString, label), Q_ARG(QString, text));
|
|
return ret;
|
|
}
|
|
|
|
QJSValue ScriptEngine::getInt(const QString &label, int value)
|
|
{
|
|
QJSValue ret;
|
|
QMetaObject::invokeMethod(_parent, "getInt", Qt::BlockingQueuedConnection, Q_RETURN_ARG(QJSValue, ret), Q_ARG(QString, label), Q_ARG(int, value));
|
|
return ret;
|
|
}
|
|
|
|
QJSValue ScriptEngine::getFloat(const QString &label, double value, int decimals) const
|
|
{
|
|
QJSValue ret;
|
|
QMetaObject::invokeMethod(_parent, "getFloat", Qt::BlockingQueuedConnection, Q_RETURN_ARG(QJSValue, ret), Q_ARG(QString, label), Q_ARG(double, value), Q_ARG(int, decimals));
|
|
return ret;
|
|
}
|
|
|
|
QJSValue ScriptEngine::getItem(const QStringList &items, const QString &label, int current) const
|
|
{
|
|
QJSValue ret;
|
|
QMetaObject::invokeMethod(_parent, "getItem", Qt::BlockingQueuedConnection, Q_RETURN_ARG(QJSValue, ret), Q_ARG(QStringList, items), Q_ARG(QString, label), Q_ARG(int, current));
|
|
return ret;
|
|
}
|
|
|
|
bool ScriptEngine::convert(File *file, QString &outpath, const QString &format, const QVariantMap ¶ms, bool async)
|
|
{
|
|
QString path;
|
|
QDir dir(_outputDir);
|
|
QFileInfo info(outpath);
|
|
QString suffix = info.suffix();
|
|
if(info.isAbsolute())
|
|
path = info.absolutePath();
|
|
else
|
|
path = dir.absoluteFilePath(outpath);
|
|
|
|
QString f = format.toLower();
|
|
if(f != "xisf" && f != "fits" && f != "png" && f != "bmp" && f != "jpg")
|
|
{
|
|
logError("Output format must be one of xisf fits jpg png bmp");
|
|
return false;
|
|
}
|
|
|
|
info.setFile(path);
|
|
outpath = info.absolutePath() + "/" + info.completeBaseName() + "." + f;
|
|
if(async)
|
|
{
|
|
_semaphore.acquire();
|
|
_pool->start(new ConvertRunable(file->absoluteFilePath(), outpath, f, params, &_semaphore));
|
|
}
|
|
else
|
|
{
|
|
ConvertRunable crun(file->absoluteFilePath(), outpath, f, params, nullptr);
|
|
crun.run();
|
|
}
|
|
return true;
|
|
}
|
|
|
|
QJSValue ScriptEngine::newObject()
|
|
{
|
|
return _jsEngine->newObject();
|
|
}
|
|
|
|
QJSValue ScriptEngine::newArray(uint size)
|
|
{
|
|
return _jsEngine->newArray(size);
|
|
}
|
|
|
|
void ScriptEngine::run()
|
|
{
|
|
QJSValue jsPaths = _jsEngine->newArray(_paths.size());
|
|
for(qsizetype i=0; i<_paths.size(); i++)
|
|
jsPaths.setProperty(i, _jsEngine->newQObject(new File(_paths[i].first, _paths[i].second, this)));
|
|
|
|
_jsEngine->globalObject().setProperty("files", jsPaths);
|
|
|
|
QFile scriptFile(_scriptPath);
|
|
if(!scriptFile.open(QIODevice::ReadOnly))
|
|
{
|
|
emit newMessage("Failed to open " + _scriptPath, true);
|
|
emit finished();
|
|
return;
|
|
}
|
|
|
|
QTextStream stream(&scriptFile);
|
|
QString contents = stream.readAll();
|
|
scriptFile.close();
|
|
QJSValue result = _jsEngine->evaluate(contents, _scriptPath);
|
|
qDebug() << result.isError() << result.toString();
|
|
_pool->waitForDone();
|
|
if(result.isError())
|
|
{
|
|
QString error = result.property("name").toString() + " on line " + result.property("lineNumber").toString() + " : " + result.toString();
|
|
error += "\n" + result.property("stack").toString();
|
|
emit newMessage(error, true);
|
|
}
|
|
|
|
emit finished();
|
|
}
|
|
|
|
void File::loadFitsKeywords()
|
|
{
|
|
if(!_fitsKeywordsLoaded)
|
|
{
|
|
_fitsKeywordsLoaded = true;
|
|
ImageInfoData info;
|
|
if(suffix().toLower() == "xisf")
|
|
{
|
|
readXISFHeader(_path, info);
|
|
}
|
|
else if(suffix().toLower() == "fits" || suffix().toLower() == "fit")
|
|
{
|
|
readFITSHeader(_path, info);
|
|
}
|
|
else return;
|
|
|
|
for(auto &record : info.fitsHeader)
|
|
{
|
|
_fitsKeywords.append(record.key);
|
|
_fitsRecords.insert(record.key, record);
|
|
}
|
|
}
|
|
}
|
|
|
|
bool File::mkpath(const QString &path) const
|
|
{
|
|
QFileInfo info(path);
|
|
if(!info.isRelative())
|
|
{
|
|
_engine->logError("Destination path is not relative");
|
|
return false;
|
|
}
|
|
QDir dir(_engine->outputDir());
|
|
if(dir.mkpath(info.path()))
|
|
{
|
|
return true;
|
|
}
|
|
else
|
|
{
|
|
_engine->logError("Failed to create dir " + info.path());
|
|
return false;
|
|
}
|
|
}
|
|
|
|
File::File(const QString &path, Script::ScriptEngine *engine) : File(path, QString(), engine)
|
|
{
|
|
}
|
|
|
|
File::File(const QString &path, const QString &root, ScriptEngine *engine) :
|
|
_engine(engine),
|
|
_path(path),
|
|
_root(root),
|
|
_info(path)
|
|
{
|
|
}
|
|
|
|
QString File::fileName() const
|
|
{
|
|
return _info.fileName();
|
|
}
|
|
|
|
QString File::absoluteFilePath() const
|
|
{
|
|
return _info.absoluteFilePath();
|
|
}
|
|
|
|
QString File::absolutePath() const
|
|
{
|
|
return _info.absolutePath();
|
|
}
|
|
|
|
QString File::relativeFilePath() const
|
|
{
|
|
QDir dir(_root);
|
|
return dir.relativeFilePath(_info.absoluteFilePath());
|
|
}
|
|
|
|
QString File::relativePath() const
|
|
{
|
|
QDir dir(_root);
|
|
return dir.relativeFilePath(_info.absolutePath());
|
|
}
|
|
|
|
QString File::baseName() const
|
|
{
|
|
return _info.baseName();
|
|
}
|
|
|
|
QString File::completeBaseName() const
|
|
{
|
|
return _info.completeBaseName();
|
|
}
|
|
|
|
QString File::suffix() const
|
|
{
|
|
return _info.suffix();
|
|
}
|
|
|
|
qint64 File::size() const
|
|
{
|
|
return _info.size();
|
|
}
|
|
|
|
QStringList File::fitsKeywords()
|
|
{
|
|
loadFitsKeywords();
|
|
return _fitsKeywords;
|
|
}
|
|
|
|
QString File::fitsValue(const QString &key)
|
|
{
|
|
loadFitsKeywords();
|
|
if(_fitsRecords.contains(key))
|
|
return _fitsRecords[key].value.toString();
|
|
else
|
|
return QString();
|
|
}
|
|
|
|
QJSValue File::fitsValues(const QString &key)
|
|
{
|
|
loadFitsKeywords();
|
|
if(_fitsRecords.contains(key))
|
|
{
|
|
QList<FITSRecord> values = _fitsRecords.values(key);
|
|
QJSValue array = _engine->newArray(values.size());
|
|
for(qsizetype i=0; i<values.size(); i++)
|
|
array.setProperty(i, values[i].value.toString());
|
|
return array;
|
|
}
|
|
else
|
|
return QString();
|
|
}
|
|
|
|
QJSValue File::fitsRecords()
|
|
{
|
|
loadFitsKeywords();
|
|
|
|
QJSValue array = _engine->newArray(_fitsRecords.size());
|
|
uint i = 0;
|
|
for(auto &record : _fitsRecords)
|
|
{
|
|
QJSValue item = _engine->newObject();
|
|
item.setProperty("key", QString::fromUtf8(record.key));
|
|
item.setProperty("value", record.value.toString());
|
|
item.setProperty("comment", QString::fromUtf8(record.comment));
|
|
item.setProperty("xisf", record.xisf);
|
|
array.setProperty(i++, item);
|
|
}
|
|
|
|
return array;
|
|
}
|
|
|
|
bool File::modifyFITSRecords(const FITSRecordModify *modify)
|
|
{
|
|
_fitsKeywordsLoaded = false;
|
|
_fitsKeywords.clear();
|
|
|
|
if(QRegularExpression("fits?", QRegularExpression::CaseInsensitiveOption).match(suffix()).hasMatch())
|
|
{
|
|
fitsfile *file;
|
|
int status = 0;
|
|
fits_open_diskfile(&file, _path.toLocal8Bit().data(), READWRITE, &status);
|
|
int num = 0;
|
|
fits_get_num_hdus(file, &num, &status);
|
|
if(status)
|
|
{
|
|
_engine->newMessage("Failed to open FITS file", true);
|
|
return false;
|
|
}
|
|
int imgtype;
|
|
int naxis;
|
|
long naxes[3] = {0};
|
|
int type = -1;
|
|
for(int i=1; i <= num; i++)
|
|
{
|
|
fits_movabs_hdu(file, i, IMAGE_HDU, &status);
|
|
fits_get_hdu_type(file, &type, &status);
|
|
fits_get_img_param(file, 3, &imgtype, &naxis, naxes, &status);
|
|
if(type == IMAGE_HDU && naxis >= 2 && naxis <= 3 && status == 0)
|
|
break;
|
|
if(i == num)return false;
|
|
}
|
|
|
|
for(auto &remove : modify->_remove)
|
|
{
|
|
int status = 0;//we ignore errors from here
|
|
fits_delete_key(file, remove.toLatin1().data(), &status);
|
|
}
|
|
for(auto &record : modify->_update)
|
|
{
|
|
switch(record.value.typeId())
|
|
{
|
|
case QMetaType::Bool:
|
|
{
|
|
int val = record.value.toBool();
|
|
fits_update_key(file, TLOGICAL, record.key.data(), &val, record.comment.isEmpty() ? nullptr : record.comment.data(), &status);
|
|
break;
|
|
}
|
|
case QMetaType::Int:
|
|
case QMetaType::UInt:
|
|
{
|
|
long long val = record.value.toLongLong();
|
|
fits_update_key(file, TLONGLONG, record.key.data(), &val, record.comment.isEmpty() ? nullptr : record.comment.data(), &status);
|
|
break;
|
|
}
|
|
case QMetaType::QString:
|
|
{
|
|
QByteArray val = record.value.toString().toLatin1();
|
|
fits_update_key(file, TSTRING, record.key.data(), val.isEmpty() ? nullptr : val.data(), record.comment.isEmpty() ? nullptr : record.comment.data(), &status);
|
|
break;
|
|
}
|
|
case QMetaType::Float:
|
|
case QMetaType::Double:
|
|
{
|
|
double val = record.value.toDouble();
|
|
fits_update_key(file, TDOUBLE, record.key.data(), &val, record.comment.isEmpty() ? nullptr : record.comment.data(), &status);
|
|
break;
|
|
}
|
|
default:
|
|
_engine->newMessage("Unknown type for KEY " + record.key, true);
|
|
return false;
|
|
break;
|
|
}
|
|
if(status)
|
|
{
|
|
char error[100];
|
|
fits_get_errstatus(status, error);
|
|
_engine->newMessage(QString("Error when updating KEY %1 %2").arg(record.key).arg(error), true);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
for(auto &record : modify->_add)
|
|
{
|
|
switch(record.value.typeId())
|
|
{
|
|
case QMetaType::Bool:
|
|
{
|
|
int val = record.value.toBool();
|
|
fits_write_key(file, TLOGICAL, record.key.data(), &val, record.comment.isEmpty() ? nullptr : record.comment.data(), &status);
|
|
break;
|
|
}
|
|
case QMetaType::Int:
|
|
case QMetaType::UInt:
|
|
{
|
|
long long val = record.value.toLongLong();
|
|
fits_write_key(file, TLONGLONG, record.key.data(), &val, record.comment.isEmpty() ? nullptr : record.comment.data(), &status);
|
|
break;
|
|
}
|
|
case QMetaType::QString:
|
|
{
|
|
QByteArray val = record.value.toString().toLatin1();
|
|
fits_write_key(file, TSTRING, record.key.data(), val.isEmpty() ? nullptr : val.data(), record.comment.isEmpty() ? nullptr : record.comment.data(), &status);
|
|
break;
|
|
}
|
|
case QMetaType::Float:
|
|
case QMetaType::Double:
|
|
{
|
|
double val = record.value.toDouble();
|
|
fits_write_key(file, TDOUBLE, record.key.data(), &val, record.comment.isEmpty() ? nullptr : record.comment.data(), &status);
|
|
break;
|
|
}
|
|
default:
|
|
_engine->newMessage("Unknown type for KEY " + record.key, true);
|
|
return false;
|
|
break;
|
|
}
|
|
if(status)
|
|
{
|
|
char error[100];
|
|
fits_get_errstatus(status, error);
|
|
_engine->newMessage(QString("Error when adding KEY {} {}").arg(record.key).arg(error), true);
|
|
return false;
|
|
}
|
|
}
|
|
fits_close_file(file, &status);
|
|
|
|
return status == 0;
|
|
}
|
|
else if(suffix() == "xisf")
|
|
{
|
|
try
|
|
{
|
|
LibXISF::XISFModify modifyXISF;
|
|
modifyXISF.open(_path.toLocal8Bit().data());
|
|
QFileInfo in(_path);
|
|
QFileInfo out(_path + "~");
|
|
|
|
for(auto &remove : modify->_remove)
|
|
modifyXISF.removeFITSKeyword(0, remove.toStdString());
|
|
|
|
for(auto &record : modify->_update)
|
|
modifyXISF.updateFITSKeyword(0, {record.key.toStdString(), record.value.toString().toStdString(), record.value.toString().toStdString()}, true);
|
|
|
|
for(auto &record : modify->_add)
|
|
modifyXISF.addFITSKeyword(0, {record.key.toStdString(), record.value.toString().toStdString(), record.value.toString().toStdString()});
|
|
|
|
modifyXISF.save(out.absoluteFilePath().toLocal8Bit().toStdString());
|
|
modifyXISF.close();
|
|
std::filesystem::rename(out.filesystemAbsoluteFilePath(), in.filesystemAbsoluteFilePath());
|
|
}
|
|
catch(LibXISF::Error &err)
|
|
{
|
|
_engine->newMessage("Failed to modify file " + _path + " " + err.what(), true);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
bool File::isMarked() const
|
|
{
|
|
return _engine->isMarked(this);
|
|
}
|
|
|
|
File* File::copy(const QString &newpath) const
|
|
{
|
|
if(mkpath(newpath))
|
|
{
|
|
if(QFile::copy(_path, _engine->outputDir() + newpath))
|
|
return new File(_engine->outputDir() + newpath, _engine);
|
|
_engine->logError("Failed copy to " + newpath);
|
|
return nullptr;
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
bool File::move(const QString &newpath)
|
|
{
|
|
if(mkpath(newpath))
|
|
{
|
|
if(QFile::rename(_path, _engine->outputDir() + newpath))
|
|
{
|
|
_path = _engine->outputDir() + newpath;
|
|
return true;
|
|
}
|
|
_engine->logError("Failed move to " + newpath);
|
|
return false;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
File* File::convert(const QString &outpath, const QString &format, const QVariantMap ¶ms)
|
|
{
|
|
QString path = outpath;
|
|
if(_engine->convert(this, path, format, params, false))
|
|
return new File(path, _engine);
|
|
else
|
|
return nullptr;
|
|
}
|
|
|
|
File* File::convertAsync(const QString &outpath, const QString &format, const QVariantMap ¶ms)
|
|
{
|
|
QString path = outpath;
|
|
if(_engine->convert(this, path, format, params, true))
|
|
return new File(path, _engine);
|
|
else
|
|
return nullptr;
|
|
}
|
|
|
|
QJSValue File::stats()
|
|
{
|
|
if(_stats.isUndefined())
|
|
{
|
|
ImageInfoData info;
|
|
std::shared_ptr<RawImage> rawImage;
|
|
loadImage(_path, info, rawImage);
|
|
rawImage->calcStats();
|
|
RawImage::Stats stats = rawImage->imageStats();
|
|
_stats = _engine->newObject();
|
|
_stats.setProperty("mean", stats.m_mean[0]);
|
|
_stats.setProperty("stddev", stats.m_stdDev[0]);
|
|
_stats.setProperty("median", stats.m_median[0]);
|
|
_stats.setProperty("min", stats.m_min[0]);
|
|
_stats.setProperty("max", stats.m_max[0]);
|
|
_stats.setProperty("mad", stats.m_mean[0]);
|
|
}
|
|
return _stats;
|
|
}
|
|
|
|
ScriptEngineThread::ScriptEngineThread(BatchProcessing *parent) : QObject(parent)
|
|
{
|
|
_thread = new QThread();
|
|
_thread->setObjectName("ScriptEngine");
|
|
_engine = new ScriptEngine(parent);
|
|
_engine->moveToThread(_thread);
|
|
connect(_engine, &ScriptEngine::finished, _thread, &QThread::quit);
|
|
connect(_engine, &ScriptEngine::newMessage, this, &ScriptEngineThread::newMessage);
|
|
connect(_thread, &QThread::started, _engine, &ScriptEngine::run);
|
|
connect(_thread, &QThread::finished, _engine, &ScriptEngine::deleteLater);
|
|
connect(_engine, &ScriptEngine::destroyed, [this](){ _engine = nullptr; });
|
|
connect(_thread, &QThread::finished, _thread, &QThread::deleteLater);
|
|
connect(_thread, &QThread::finished, this, &ScriptEngineThread::finished);
|
|
}
|
|
|
|
ScriptEngineThread::~ScriptEngineThread()
|
|
{
|
|
if(_engine)_engine->interrupt();
|
|
}
|
|
|
|
void ScriptEngineThread::setParams(const QString &scriptPath, const QList<QPair<QString, QString>> &paths, const QString &outputDir)
|
|
{
|
|
_engine->setParams(scriptPath, paths, outputDir);
|
|
}
|
|
|
|
void ScriptEngineThread::start()
|
|
{
|
|
_thread->start();
|
|
}
|
|
|
|
void ScriptEngineThread::interrupt()
|
|
{
|
|
if(_engine)_engine->interrupt();
|
|
}
|
|
|
|
void FITSRecordModify::removeKeyword(const QString &key)
|
|
{
|
|
if(!_remove.contains(key))
|
|
_remove.append(key);
|
|
}
|
|
|
|
void FITSRecordModify::updateKeyword(const QString &key, const QVariant &value, const QString &comment)
|
|
{
|
|
_update.append({key.toLatin1(), value, comment.toLatin1()});
|
|
}
|
|
|
|
void FITSRecordModify::addKeyword(const QString &key, const QVariant &value, const QString &comment)
|
|
{
|
|
_update.append({key.toLatin1(), value, comment.toLatin1()});
|
|
}
|
|
|
|
}
|