File indexing completed on 2024-12-22 03:35:51

0001 /*
0002     File                 : OriginProjectParser.h
0003     Project              : LabPlot
0004     Description          : parser for Origin projects
0005     --------------------------------------------------------------------
0006     SPDX-FileCopyrightText: 2017-2024 Alexander Semke <alexander.semke@web.de>
0007     SPDX-FileCopyrightText: 2017-2024 Stefan Gerlach <stefan.gerlach@uni.kn>
0008 
0009     SPDX-License-Identifier: GPL-2.0-or-later
0010 */
0011 
0012 #include "backend/datasources/projects/OriginProjectParser.h"
0013 #include "backend/core/Project.h"
0014 #include "backend/core/Workbook.h"
0015 #include "backend/core/column/Column.h"
0016 #include "backend/core/datatypes/DateTime2StringFilter.h"
0017 #include "backend/core/datatypes/Double2StringFilter.h"
0018 #include "backend/matrix/Matrix.h"
0019 #include "backend/note/Note.h"
0020 #include "backend/spreadsheet/Spreadsheet.h"
0021 #include "backend/worksheet/Line.h"
0022 #include "backend/worksheet/TextLabel.h"
0023 #include "backend/worksheet/Worksheet.h"
0024 #include "backend/worksheet/WorksheetElement.h"
0025 #include "backend/worksheet/plots/PlotArea.h"
0026 #include "backend/worksheet/plots/cartesian/Axis.h"
0027 #include "backend/worksheet/plots/cartesian/CartesianPlot.h"
0028 #include "backend/worksheet/plots/cartesian/CartesianPlotLegend.h"
0029 #include "backend/worksheet/plots/cartesian/Symbol.h"
0030 #include "backend/worksheet/plots/cartesian/XYCurve.h"
0031 #include "backend/worksheet/plots/cartesian/XYEquationCurve.h"
0032 
0033 #include <KLocalizedString>
0034 
0035 #include <QDateTime>
0036 #include <QDir>
0037 #include <QFontMetrics>
0038 #include <QGraphicsScene>
0039 #include <QRegularExpression>
0040 
0041 #include <gsl/gsl_const_cgs.h>
0042 
0043 /*!
0044 \class OriginProjectParser
0045 \brief parser for Origin projects.
0046 
0047 \ingroup datasources
0048 */
0049 
0050 OriginProjectParser::OriginProjectParser()
0051     : ProjectParser() {
0052     m_topLevelClasses = {AspectType::Folder, AspectType::Workbook, AspectType::Spreadsheet, AspectType::Matrix, AspectType::Worksheet, AspectType::Note};
0053 }
0054 
0055 OriginProjectParser::~OriginProjectParser() {
0056     delete m_originFile;
0057 }
0058 
0059 bool OriginProjectParser::isOriginProject(const QString& fileName) {
0060     // TODO add opju later when liborigin supports it
0061     return fileName.endsWith(QLatin1String(".opj"), Qt::CaseInsensitive);
0062 }
0063 
0064 void OriginProjectParser::setImportUnusedObjects(bool importUnusedObjects) {
0065     m_importUnusedObjects = importUnusedObjects;
0066 }
0067 
0068 void OriginProjectParser::checkContent(bool& hasUnusedObjects, bool& hasMultiLayerGraphs) {
0069     DEBUG(Q_FUNC_INFO)
0070     m_originFile = new OriginFile(qPrintable(m_projectFileName));
0071     if (!m_originFile->parse()) {
0072         delete m_originFile;
0073         m_originFile = nullptr;
0074         hasUnusedObjects = false;
0075         hasMultiLayerGraphs = false;
0076         return;
0077     }
0078 
0079     hasUnusedObjects = this->hasUnusedObjects();
0080     hasMultiLayerGraphs = this->hasMultiLayerGraphs();
0081 
0082     delete m_originFile;
0083     m_originFile = nullptr;
0084 }
0085 
0086 bool OriginProjectParser::hasUnusedObjects() {
0087     if (!m_originFile)
0088         return false;
0089 
0090     for (unsigned int i = 0; i < m_originFile->spreadCount(); i++) {
0091         const auto& spread = m_originFile->spread(i);
0092         if (spread.objectID < 0)
0093             return true;
0094     }
0095     for (unsigned int i = 0; i < m_originFile->excelCount(); i++) {
0096         const auto& excel = m_originFile->excel(i);
0097         if (excel.objectID < 0)
0098             return true;
0099     }
0100     for (unsigned int i = 0; i < m_originFile->matrixCount(); i++) {
0101         const auto& matrix = m_originFile->matrix(i);
0102         if (matrix.objectID < 0)
0103             return true;
0104     }
0105 
0106     return false;
0107 }
0108 
0109 bool OriginProjectParser::hasMultiLayerGraphs() {
0110     if (!m_originFile)
0111         return false;
0112 
0113     for (unsigned int i = 0; i < m_originFile->graphCount(); i++) {
0114         const auto& graph = m_originFile->graph(i);
0115         if (graph.layers.size() > 1)
0116             return true;
0117     }
0118 
0119     return false;
0120 }
0121 
0122 void OriginProjectParser::setGraphLayerAsPlotArea(bool value) {
0123     m_graphLayerAsPlotArea = value;
0124 }
0125 
0126 QString OriginProjectParser::supportedExtensions() {
0127     // TODO add opju later when liborigin supports it
0128     static const QString extensions = QStringLiteral("*.opj *.OPJ");
0129     return extensions;
0130 }
0131 
0132 // sets first found spread of given name
0133 unsigned int OriginProjectParser::findSpreadsheetByName(const QString& name) {
0134     DEBUG(Q_FUNC_INFO << ", name = " << name.toStdString() << ", count = " << m_originFile->spreadCount())
0135     for (unsigned int i = 0; i < m_originFile->spreadCount(); i++) {
0136         const auto& spread = m_originFile->spread(i);
0137         DEBUG(Q_FUNC_INFO << ", spreadsheet name = " << spread.name)
0138         if (spread.name == name.toStdString()) {
0139             m_spreadsheetNameList << name;
0140             m_spreadsheetNameList.removeDuplicates();
0141             return i;
0142         }
0143     }
0144     return 0;
0145 }
0146 unsigned int OriginProjectParser::findColumnByName(Origin::SpreadSheet& spread, const QString& name) {
0147     for (unsigned int i = 0; i < spread.columns.size(); i++) {
0148         auto column = spread.columns[i];
0149         if (column.name == name.toStdString())
0150             return i;
0151     }
0152     return 0;
0153 }
0154 unsigned int OriginProjectParser::findMatrixByName(const QString& name) {
0155     for (unsigned int i = 0; i < m_originFile->matrixCount(); i++) {
0156         const auto& originMatrix = m_originFile->matrix(i);
0157         if (originMatrix.name == name.toStdString()) {
0158             m_matrixNameList << name;
0159             m_matrixNameList.removeDuplicates();
0160             return i;
0161         }
0162     }
0163     return 0;
0164 }
0165 unsigned int OriginProjectParser::findWorkbookByName(const QString& name) {
0166     // QDEBUG("WORKBOOK LIST: " << m_workbookNameList << ", name = " << name)
0167     for (unsigned int i = 0; i < m_originFile->excelCount(); i++) {
0168         const auto& excel = m_originFile->excel(i);
0169         if (excel.name == name.toStdString()) {
0170             m_workbookNameList << name;
0171             m_workbookNameList.removeDuplicates();
0172             return i;
0173         }
0174     }
0175     return 0;
0176 }
0177 unsigned int OriginProjectParser::findWorksheetByName(const QString& name) {
0178     for (unsigned int i = 0; i < m_originFile->graphCount(); i++) {
0179         const auto& graph = m_originFile->graph(i);
0180         if (graph.name == name.toStdString()) {
0181             m_worksheetNameList << name;
0182             m_worksheetNameList.removeDuplicates();
0183             return i;
0184         }
0185     }
0186     return 0;
0187 }
0188 unsigned int OriginProjectParser::findNoteByName(const QString& name) {
0189     for (unsigned int i = 0; i < m_originFile->noteCount(); i++) {
0190         const auto& originNote = m_originFile->note(i);
0191         if (originNote.name == name.toStdString()) {
0192             m_noteNameList << name;
0193             m_noteNameList.removeDuplicates();
0194             return i;
0195         }
0196     }
0197     return 0;
0198 }
0199 
0200 // get Origin::Spreadsheet from container name (may be a spreadsheet or workbook)
0201 Origin::SpreadSheet OriginProjectParser::getSpreadsheetByName(QString& containerName) {
0202     DEBUG(Q_FUNC_INFO)
0203     int sheetIndex = 0; // which sheet? "@X"
0204     const int atIndex = containerName.indexOf(QLatin1Char('@'));
0205     if (atIndex != -1) {
0206         sheetIndex = containerName.mid(atIndex + 1).toInt() - 1;
0207         containerName.truncate(atIndex);
0208     }
0209     // DEBUG("CONTAINER = " << STDSTRING(containerName) << ", SHEET = " << sheetIndex)
0210 
0211     // check if workbook
0212     int workbookIndex = findWorkbookByName(containerName);
0213     // if workbook not found, findWorkbookByName() returns 0: check this
0214     if (workbookIndex == 0 && (m_originFile->excelCount() == 0 || containerName.toStdString() != m_originFile->excel(0).name))
0215         workbookIndex = -1;
0216     // DEBUG("WORKBOOK  index = " << workbookIndex)
0217 
0218     // comment of y column is used in legend (if not empty), else the column name
0219     Origin::SpreadSheet sheet;
0220     if (workbookIndex != -1) { // container is a workbook
0221         sheet = m_originFile->excel(workbookIndex).sheets[sheetIndex];
0222     } else { // container is a spreadsheet?
0223         int spreadsheetIndex = findSpreadsheetByName(containerName);
0224         // if spreadsheet not found, findSpreadsheetByName() returns 0: check this
0225         if (spreadsheetIndex == 0 && (m_originFile->spreadCount() == 0 || containerName.toStdString() != m_originFile->spread(0).name))
0226             spreadsheetIndex = -1;
0227         if (spreadsheetIndex != -1)
0228             sheet = m_originFile->spread(spreadsheetIndex);
0229     }
0230 
0231     return sheet;
0232 }
0233 
0234 // ##############################################################################
0235 // ############## Deserialization from Origin's project tree ####################
0236 // ##############################################################################
0237 bool OriginProjectParser::load(Project* project, bool preview) {
0238     DEBUG(Q_FUNC_INFO);
0239 
0240     // read and parse the m_originFile-file
0241     m_originFile = new OriginFile(qPrintable(m_projectFileName));
0242     if (!m_originFile->parse()) {
0243         delete m_originFile;
0244         m_originFile = nullptr;
0245         return false;
0246     }
0247 
0248     DEBUG(Q_FUNC_INFO << ", project file name: " << m_projectFileName.toStdString());
0249     DEBUG(Q_FUNC_INFO << ", Origin version: " << m_originFile->version());
0250 
0251     // Origin project tree and the iterator pointing to the root node
0252     const auto* projectTree = m_originFile->project();
0253     auto projectIt = projectTree->begin(projectTree->begin());
0254 
0255     m_spreadsheetNameList.clear();
0256     m_workbookNameList.clear();
0257     m_matrixNameList.clear();
0258     m_worksheetNameList.clear();
0259     m_noteNameList.clear();
0260 
0261     // convert the project tree from liborigin's representation to LabPlot's project object
0262     project->setIsLoading(true);
0263     if (projectIt.node) { // only opj files from version >= 6.0 have a project tree
0264         DEBUG(Q_FUNC_INFO << ", project tree found");
0265         QString name(QString::fromLatin1(projectIt->name.c_str()));
0266         project->setName(name);
0267         project->setCreationTime(creationTime(projectIt));
0268         loadFolder(project, projectIt, preview);
0269     } else { // for older versions put all windows on rootfolder
0270         DEBUG(Q_FUNC_INFO << ", no project tree");
0271         int pos = m_projectFileName.lastIndexOf(QLatin1Char('/')) + 1;
0272         project->setName(m_projectFileName.mid(pos));
0273     }
0274     // imports all loose windows (like prior version 6 which has no project tree)
0275     handleLooseWindows(project, preview);
0276 
0277     // restore column pointers:
0278     // 1. extend the pathes to contain the parent structures first
0279     // 2. restore the pointers from the pathes
0280     const auto& columns = project->children<Column>(AbstractAspect::ChildIndexFlag::Recursive);
0281     const auto& spreadsheets = project->children<Spreadsheet>(AbstractAspect::ChildIndexFlag::Recursive);
0282     const auto& curves = project->children<XYCurve>(AbstractAspect::ChildIndexFlag::Recursive);
0283     DEBUG(Q_FUNC_INFO << ", NUMBER of spreadsheets/columns = "
0284                       << "/" << spreadsheets.count() << "/" << columns.count())
0285     for (auto* curve : curves) {
0286         DEBUG(Q_FUNC_INFO << ", RESTORE CURVE with x/y column path " << STDSTRING(curve->xColumnPath()) << " " << STDSTRING(curve->yColumnPath()))
0287         curve->setSuppressRetransform(true);
0288 
0289         // x-column
0290         QString spreadsheetName = curve->xColumnPath();
0291         spreadsheetName.truncate(curve->xColumnPath().lastIndexOf(QLatin1Char('/')));
0292         // DEBUG(Q_FUNC_INFO << ", SPREADSHEET name from column: " << STDSTRING(spreadsheetName))
0293         for (const auto* spreadsheet : spreadsheets) {
0294             QString container, containerPath = spreadsheet->parentAspect()->path();
0295             if (spreadsheetName.contains(QLatin1Char('/'))) { // part of a workbook
0296                 container = containerPath.mid(containerPath.lastIndexOf(QLatin1Char('/')) + 1) + QLatin1Char('/');
0297                 containerPath = containerPath.left(containerPath.lastIndexOf(QLatin1Char('/')));
0298             }
0299             // DEBUG("CONTAINER = " << STDSTRING(container))
0300             // DEBUG("CONTAINER PATH = " << STDSTRING(containerPath))
0301             // DEBUG(Q_FUNC_INFO << ", LOOP spreadsheet names = \"" << STDSTRING(container) +
0302             //  STDSTRING(spreadsheet->name()) << "\", path = " << STDSTRING(spreadsheetName))
0303             // DEBUG("SPREADSHEET parent path = " << STDSTRING(spreadsheet->parentAspect()->path()))
0304             if (container + spreadsheet->name() == spreadsheetName) {
0305                 const QString& newPath = containerPath + QLatin1Char('/') + curve->xColumnPath();
0306                 // const QString& newPath = QLatin1String("Project") + QLatin1Char('/') + curve->xColumnPath();
0307                 DEBUG(Q_FUNC_INFO << ", SET COLUMN PATH to \"" << STDSTRING(newPath) << "\"")
0308                 curve->setXColumnPath(newPath);
0309 
0310                 for (auto* column : columns) {
0311                     if (!column)
0312                         continue;
0313                     if (column->path() == newPath) {
0314                         // DEBUG(Q_FUNC_INFO << ", set X column path = \"" << STDSTRING(column->path()) << "\"")
0315                         curve->setXColumn(column);
0316                         break;
0317                     }
0318                 }
0319                 break;
0320             }
0321         }
0322 
0323         // y-column
0324         spreadsheetName = curve->yColumnPath();
0325         spreadsheetName.truncate(curve->yColumnPath().lastIndexOf(QLatin1Char('/')));
0326         for (const auto* spreadsheet : spreadsheets) {
0327             QString container, containerPath = spreadsheet->parentAspect()->path();
0328             if (spreadsheetName.contains(QLatin1Char('/'))) { // part of a workbook
0329                 container = containerPath.mid(containerPath.lastIndexOf(QLatin1Char('/')) + 1) + QLatin1Char('/');
0330                 containerPath = containerPath.left(containerPath.lastIndexOf(QLatin1Char('/')));
0331             }
0332             if (container + spreadsheet->name() == spreadsheetName) {
0333                 const QString& newPath = containerPath + QLatin1Char('/') + curve->yColumnPath();
0334                 curve->setYColumnPath(newPath);
0335 
0336                 for (auto* column : columns) {
0337                     if (!column)
0338                         continue;
0339                     // DEBUG(Q_FUNC_INFO << ", column paths = \"" << STDSTRING(column->path())
0340                     //  << "\" / \"" << STDSTRING(newPath) << "\"" )
0341                     if (column->path() == newPath) {
0342                         curve->setYColumn(column);
0343                         break;
0344                     }
0345                 }
0346                 break;
0347             }
0348         }
0349         DEBUG(Q_FUNC_INFO << ", curve x/y COLUMNS = " << curve->xColumn() << "/" << curve->yColumn())
0350 
0351         // TODO: error columns
0352 
0353         curve->setSuppressRetransform(false);
0354     }
0355 
0356     if (!preview) {
0357         const auto& plots = project->children<CartesianPlot>(AbstractAspect::ChildIndexFlag::Recursive);
0358         for (auto* plot : plots) {
0359             plot->setIsLoading(false);
0360             plot->retransform();
0361         }
0362     }
0363 
0364     project->setIsLoading(false);
0365 
0366     delete m_originFile;
0367     m_originFile = nullptr;
0368 
0369     return true;
0370 }
0371 
0372 bool OriginProjectParser::loadFolder(Folder* folder, tree<Origin::ProjectNode>::iterator baseIt, bool preview) {
0373     DEBUG(Q_FUNC_INFO)
0374     const tree<Origin::ProjectNode>* projectTree = m_originFile->project();
0375 
0376     // do not skip anything if pathesToLoad() contains only root folder
0377     bool containsRootFolder = (folder->pathesToLoad().size() == 1 && folder->pathesToLoad().contains(folder->path()));
0378     if (containsRootFolder) {
0379         DEBUG(" pathesToLoad contains only folder path \"" << STDSTRING(folder->path()) << "\". Clearing pathes to load.")
0380         folder->setPathesToLoad(QStringList());
0381     }
0382 
0383     // load folder's children: logic for reading the selected objects only is similar to Folder::readChildAspectElement
0384     for (auto it = projectTree->begin(baseIt); it != projectTree->end(baseIt); ++it) {
0385         QString name(QString::fromLatin1(it->name.c_str())); // name of the current child
0386         DEBUG(Q_FUNC_INFO << ", folder item name = " << STDSTRING(name))
0387 
0388         // check whether we need to skip the loading of the current child
0389         if (!folder->pathesToLoad().isEmpty()) {
0390             // child's path is not available yet (child not added yet) -> construct the path manually
0391             const QString childPath = folder->path() + QLatin1Char('/') + name;
0392             DEBUG("     path = " << STDSTRING(childPath))
0393 
0394             // skip the current child aspect it is not in the list of aspects to be loaded
0395             if (folder->pathesToLoad().indexOf(childPath) == -1) {
0396                 DEBUG("     skip it!")
0397                 continue;
0398             }
0399         }
0400 
0401         // load top-level children.
0402         // use 'preview' as 'loading'-parameter in the constructors to skip the init() calls in Worksheet, Spreadsheet and Matrix:
0403         //* when doing the preview of the project we don't want to initialize the objects and skip init()'s
0404         //* when loading the project, 'preview' is false and we initialize all objects with our default values
0405         //   and set all possible properties from Origin additionally
0406         AbstractAspect* aspect = nullptr;
0407         switch (it->type) {
0408         case Origin::ProjectNode::Folder: {
0409             DEBUG(Q_FUNC_INFO << ", top level FOLDER");
0410             Folder* f = new Folder(name);
0411 
0412             if (!folder->pathesToLoad().isEmpty()) {
0413                 // a child folder to be read -> provide the list of aspects to be loaded to the child folder, too.
0414                 // since the child folder and all its children are not added yet (path() returns empty string),
0415                 // we need to remove the path of the current child folder from the full pathes provided in pathesToLoad.
0416                 // E.g. we want to import the path "Project/Folder/Spreadsheet" in the following project
0417                 //  Project
0418                 //         \Spreadsheet
0419                 //         \Folder
0420                 //                \Spreadsheet
0421                 //
0422                 // Here, we remove the part "Project/Folder/" and proceed for this child folder with "Spreadsheet" only.
0423                 // With this the logic above where it is determined whether to import the child aspect or not works out.
0424 
0425                 // manually construct the path of the child folder to be read
0426                 const QString& curFolderPath = folder->path() + QLatin1Char('/') + name;
0427 
0428                 // remove the path of the current child folder
0429                 QStringList pathesToLoadNew;
0430                 for (const auto& path : folder->pathesToLoad()) {
0431                     if (path.startsWith(curFolderPath))
0432                         pathesToLoadNew << path.right(path.length() - curFolderPath.length());
0433                 }
0434 
0435                 f->setPathesToLoad(pathesToLoadNew);
0436             }
0437 
0438             loadFolder(f, it, preview);
0439             aspect = f;
0440             break;
0441         }
0442         case Origin::ProjectNode::SpreadSheet: {
0443             DEBUG(Q_FUNC_INFO << ", top level SPREADSHEET");
0444             auto* spreadsheet = new Spreadsheet(name, preview);
0445             loadSpreadsheet(spreadsheet, preview, name);
0446             aspect = spreadsheet;
0447             break;
0448         }
0449         case Origin::ProjectNode::Graph: {
0450             DEBUG(Q_FUNC_INFO << ", top level GRAPH");
0451             auto* worksheet = new Worksheet(name, preview);
0452             if (!preview) {
0453                 worksheet->setIsLoading(true);
0454                 worksheet->setTheme(QString());
0455             }
0456             loadWorksheet(worksheet, preview);
0457             aspect = worksheet;
0458             break;
0459         }
0460         case Origin::ProjectNode::Matrix: {
0461             DEBUG(Q_FUNC_INFO << ", top level MATRIX");
0462             const auto& originMatrix = m_originFile->matrix(findMatrixByName(name));
0463             DEBUG(" matrix name = " << originMatrix.name);
0464             DEBUG(" number of sheets = " << originMatrix.sheets.size());
0465             if (originMatrix.sheets.size() == 1) {
0466                 // single sheet -> load into a matrix
0467                 Matrix* matrix = new Matrix(name, preview);
0468                 loadMatrix(matrix, preview);
0469                 aspect = matrix;
0470             } else {
0471                 // multiple sheets -> load into a workbook
0472                 Workbook* workbook = new Workbook(name);
0473                 loadMatrixWorkbook(workbook, preview);
0474                 aspect = workbook;
0475             }
0476             break;
0477         }
0478         case Origin::ProjectNode::Excel: {
0479             DEBUG(Q_FUNC_INFO << ", top level WORKBOOK");
0480             auto* workbook = new Workbook(name);
0481             loadWorkbook(workbook, preview);
0482             aspect = workbook;
0483             break;
0484         }
0485         case Origin::ProjectNode::Note: {
0486             DEBUG(Q_FUNC_INFO << ", top level NOTE");
0487             Note* note = new Note(name);
0488             loadNote(note, preview);
0489             aspect = note;
0490             break;
0491         }
0492         case Origin::ProjectNode::Graph3D:
0493         default:
0494             // TODO: add UnsupportedAspect
0495             break;
0496         }
0497 
0498         if (aspect) {
0499             folder->addChildFast(aspect);
0500             aspect->setCreationTime(creationTime(it));
0501             aspect->setIsLoading(false);
0502         }
0503     }
0504 
0505     // ResultsLog
0506     QString resultsLog = QString::fromStdString(m_originFile->resultsLogString());
0507     if (resultsLog.length() > 0) {
0508         DEBUG("Results log:\t\tyes");
0509         Note* note = new Note(QStringLiteral("ResultsLog"));
0510 
0511         if (preview)
0512             folder->addChildFast(note);
0513         else {
0514             // only import the log if it is in the list of aspects to be loaded
0515             const QString childPath = folder->path() + QLatin1Char('/') + note->name();
0516             if (folder->pathesToLoad().indexOf(childPath) != -1) {
0517                 note->setText(resultsLog);
0518                 folder->addChildFast(note);
0519             }
0520         }
0521     } else
0522         DEBUG("Results log:\t\tno");
0523 
0524     return folder;
0525 }
0526 
0527 void OriginProjectParser::handleLooseWindows(Folder* folder, bool preview) {
0528     QDEBUG(Q_FUNC_INFO << ", paths to load:" << folder->pathesToLoad());
0529     QDEBUG("    spreads =" << m_spreadsheetNameList);
0530     QDEBUG("    workbooks =" << m_workbookNameList);
0531     QDEBUG("    matrices =" << m_matrixNameList);
0532     QDEBUG("    worksheets =" << m_worksheetNameList);
0533     QDEBUG("    notes =" << m_noteNameList);
0534 
0535     DEBUG("Number of spreads loaded:\t" << m_spreadsheetNameList.size() << ", in file: " << m_originFile->spreadCount());
0536     DEBUG("Number of excels loaded:\t" << m_workbookNameList.size() << ", in file: " << m_originFile->excelCount());
0537     DEBUG("Number of matrices loaded:\t" << m_matrixNameList.size() << ", in file: " << m_originFile->matrixCount());
0538     DEBUG("Number of graphs loaded:\t" << m_worksheetNameList.size() << ", in file: " << m_originFile->graphCount());
0539     DEBUG("Number of notes loaded:\t\t" << m_noteNameList.size() << ", in file: " << m_originFile->noteCount());
0540 
0541     // loop over all spreads to find loose ones
0542     for (unsigned int i = 0; i < m_originFile->spreadCount(); i++) {
0543         AbstractAspect* aspect = nullptr;
0544         const auto& spread = m_originFile->spread(i);
0545         QString name = QString::fromStdString(spread.name);
0546 
0547         DEBUG(" spread.objectId = " << spread.objectID);
0548         // skip unused spreads if selected
0549         if (spread.objectID < 0 && !m_importUnusedObjects) {
0550             DEBUG(" Dropping unused loose spread: " << STDSTRING(name));
0551             continue;
0552         }
0553 
0554         const QString childPath = folder->path() + QLatin1Char('/') + name;
0555         // we could also use spread.loose
0556         if (!m_spreadsheetNameList.contains(name) && (preview || folder->pathesToLoad().indexOf(childPath) != -1)) {
0557             DEBUG(" Adding loose spread: " << STDSTRING(name));
0558 
0559             auto* spreadsheet = new Spreadsheet(name);
0560             loadSpreadsheet(spreadsheet, preview, name);
0561             aspect = spreadsheet;
0562         }
0563         if (aspect) {
0564             folder->addChildFast(aspect);
0565             DEBUG(" creation time as reported by liborigin: " << spread.creationDate);
0566             aspect->setCreationTime(QDateTime::fromSecsSinceEpoch(spread.creationDate));
0567         }
0568     }
0569     // loop over all workbooks to find loose ones
0570     for (unsigned int i = 0; i < m_originFile->excelCount(); i++) {
0571         AbstractAspect* aspect = nullptr;
0572         const auto& excel = m_originFile->excel(i);
0573         QString name = QString::fromStdString(excel.name);
0574 
0575         DEBUG(" excel.objectId = " << excel.objectID);
0576         // skip unused data sets if selected
0577         if (excel.objectID < 0 && !m_importUnusedObjects) {
0578             DEBUG(" Dropping unused loose excel: " << STDSTRING(name));
0579             continue;
0580         }
0581 
0582         const QString childPath = folder->path() + QLatin1Char('/') + name;
0583         // we could also use excel.loose
0584         if (!m_workbookNameList.contains(name) && (preview || folder->pathesToLoad().indexOf(childPath) != -1)) {
0585             DEBUG(" Adding loose excel: " << STDSTRING(name));
0586             DEBUG("  containing number of sheets = " << excel.sheets.size());
0587 
0588             auto* workbook = new Workbook(name);
0589             loadWorkbook(workbook, preview);
0590             aspect = workbook;
0591         }
0592         if (aspect) {
0593             folder->addChildFast(aspect);
0594             DEBUG(" creation time as reported by liborigin: " << excel.creationDate);
0595             aspect->setCreationTime(QDateTime::fromSecsSinceEpoch(excel.creationDate));
0596         }
0597     }
0598     // loop over all matrices to find loose ones
0599     for (unsigned int i = 0; i < m_originFile->matrixCount(); i++) {
0600         AbstractAspect* aspect = nullptr;
0601         const auto& originMatrix = m_originFile->matrix(i);
0602         QString name = QString::fromStdString(originMatrix.name);
0603 
0604         DEBUG(" originMatrix.objectId = " << originMatrix.objectID);
0605         // skip unused data sets if selected
0606         if (originMatrix.objectID < 0 && !m_importUnusedObjects) {
0607             DEBUG(" Dropping unused loose matrix: " << STDSTRING(name));
0608             continue;
0609         }
0610 
0611         const QString childPath = folder->path() + QLatin1Char('/') + name;
0612         if (!m_matrixNameList.contains(name) && (preview || folder->pathesToLoad().indexOf(childPath) != -1)) {
0613             DEBUG(" Adding loose matrix: " << STDSTRING(name));
0614             DEBUG(" containing number of sheets = " << originMatrix.sheets.size());
0615             if (originMatrix.sheets.size() == 1) { // single sheet -> load into a matrix
0616                 auto* matrix = new Matrix(name);
0617                 loadMatrix(matrix, preview);
0618                 aspect = matrix;
0619             } else { // multiple sheets -> load into a workbook
0620                 auto* workbook = new Workbook(name);
0621                 loadMatrixWorkbook(workbook, preview);
0622                 aspect = workbook;
0623             }
0624         }
0625         if (aspect) {
0626             folder->addChildFast(aspect);
0627             aspect->setCreationTime(QDateTime::fromSecsSinceEpoch(originMatrix.creationDate));
0628         }
0629     }
0630     // handle loose graphs (is this even possible?)
0631     for (unsigned int i = 0; i < m_originFile->graphCount(); i++) {
0632         AbstractAspect* aspect = nullptr;
0633         const auto& graph = m_originFile->graph(i);
0634         QString name = QString::fromStdString(graph.name);
0635 
0636         DEBUG(" graph.objectId = " << graph.objectID);
0637         // skip unused graph if selected
0638         if (graph.objectID < 0 && !m_importUnusedObjects) {
0639             DEBUG(" Dropping unused loose graph: " << STDSTRING(name));
0640             continue;
0641         }
0642 
0643         const QString childPath = folder->path() + QLatin1Char('/') + name;
0644         if (!m_worksheetNameList.contains(name) && (preview || folder->pathesToLoad().indexOf(childPath) != -1)) {
0645             DEBUG(" Adding loose graph: " << STDSTRING(name));
0646             auto* worksheet = new Worksheet(name);
0647             loadWorksheet(worksheet, preview);
0648             aspect = worksheet;
0649         }
0650         if (aspect) {
0651             folder->addChildFast(aspect);
0652             aspect->setCreationTime(QDateTime::fromSecsSinceEpoch(graph.creationDate));
0653         }
0654     }
0655     // handle loose notes (is this even possible?)
0656     for (unsigned int i = 0; i < m_originFile->noteCount(); i++) {
0657         AbstractAspect* aspect = nullptr;
0658         const auto& originNote = m_originFile->note(i);
0659         QString name = QString::fromStdString(originNote.name);
0660 
0661         DEBUG(" originNote.objectId = " << originNote.objectID);
0662         // skip unused notes if selected
0663         if (originNote.objectID < 0 && !m_importUnusedObjects) {
0664             DEBUG(" Dropping unused loose note: " << STDSTRING(name));
0665             continue;
0666         }
0667 
0668         const QString childPath = folder->path() + QLatin1Char('/') + name;
0669         if (!m_noteNameList.contains(name) && (preview || folder->pathesToLoad().indexOf(childPath) != -1)) {
0670             DEBUG(" Adding loose note: " << STDSTRING(name));
0671             Note* note = new Note(name);
0672             loadNote(note, preview);
0673             aspect = note;
0674         }
0675         if (aspect) {
0676             folder->addChildFast(aspect);
0677             aspect->setCreationTime(QDateTime::fromSecsSinceEpoch(originNote.creationDate));
0678         }
0679     }
0680 }
0681 
0682 bool OriginProjectParser::loadWorkbook(Workbook* workbook, bool preview) {
0683     DEBUG(Q_FUNC_INFO);
0684     // load workbook sheets
0685     const auto& excel = m_originFile->excel(findWorkbookByName(workbook->name()));
0686     DEBUG(Q_FUNC_INFO << ", workbook name = " << excel.name);
0687     DEBUG(Q_FUNC_INFO << ", number of sheets = " << excel.sheets.size());
0688     for (unsigned int s = 0; s < excel.sheets.size(); ++s) {
0689         // DEBUG(Q_FUNC_INFO << ", LOADING SHEET " << excel.sheets[s].name)
0690         auto* spreadsheet = new Spreadsheet(QString::fromStdString(excel.sheets[s].name));
0691         loadSpreadsheet(spreadsheet, preview, workbook->name(), s);
0692         workbook->addChildFast(spreadsheet);
0693     }
0694 
0695     return true;
0696 }
0697 
0698 // load spreadsheet from spread (sheetIndex == -1) or from workbook (only sheet sheetIndex)
0699 // name is the spreadsheet name (spread) or the workbook name (if inside a workbook)
0700 bool OriginProjectParser::loadSpreadsheet(Spreadsheet* spreadsheet, bool preview, const QString& name, int sheetIndex) {
0701     DEBUG(Q_FUNC_INFO << ", own/workbook name = " << STDSTRING(name) << ", sheet index = " << sheetIndex);
0702 
0703     // load spreadsheet data
0704     Origin::SpreadSheet spread;
0705     Origin::Excel excel;
0706     if (sheetIndex == -1) // spread
0707         spread = m_originFile->spread(findSpreadsheetByName(name));
0708     else {
0709         excel = m_originFile->excel(findWorkbookByName(name));
0710         spread = excel.sheets.at(sheetIndex);
0711     }
0712 
0713     const size_t cols = spread.columns.size();
0714     int rows = 0;
0715     for (size_t j = 0; j < cols; ++j)
0716         rows = std::max((int)spread.columns.at(j).data.size(), rows);
0717     // alternative: int rows = excel.maxRows;
0718     DEBUG(Q_FUNC_INFO << ", cols/maxRows = " << cols << "/" << rows);
0719 
0720     // TODO QLocale locale = mw->locale();
0721 
0722     spreadsheet->setRowCount(rows);
0723     spreadsheet->setColumnCount((int)cols);
0724     if (sheetIndex == -1)
0725         spreadsheet->setComment(QString::fromStdString(spread.label));
0726     else // TODO: only first spread should get the comments
0727         spreadsheet->setComment(QString::fromStdString(excel.label));
0728 
0729     // in Origin column width is measured in characters, we need to convert to pixels
0730     // TODO: determine the font used in Origin in order to get the same column width as in Origin
0731     QFont font;
0732     QFontMetrics fm(font);
0733     const int scaling_factor = fm.maxWidth();
0734 
0735     for (size_t j = 0; j < cols; ++j) {
0736         auto column = spread.columns[j];
0737         auto* col = spreadsheet->column((int)j);
0738 
0739         DEBUG(Q_FUNC_INFO << ", column " << j << ", name = " << column.name << ", dataset name = " << column.dataset_name)
0740         QString name(QString::fromStdString(column.name));
0741         col->setName(name.remove(QRegularExpression(QStringLiteral(".*_"))));
0742 
0743         if (preview)
0744             continue;
0745 
0746         // TODO: we don't support any formulas for cells yet.
0747         DEBUG(Q_FUNC_INFO << ", column " << j << ", command = " << column.command)
0748         //      if (column.command.size() > 0)
0749         //          col->setFormula(Interval<int>(0, rows), QString::fromStdString(column.command));
0750 
0751         DEBUG(Q_FUNC_INFO << ", column " << j << ", full comment = " << column.comment)
0752         QString comment;
0753         if (m_originFile->version() < 9.5) // <= 2017 : pre-Unicode
0754             comment = QString::fromLatin1(column.comment.c_str());
0755         else
0756             comment = QString::fromStdString(column.comment);
0757         if (comment.contains(QLatin1Char('@'))) // remove @ options
0758             comment.truncate(comment.indexOf(QLatin1Char('@')));
0759         col->setComment(comment);
0760         col->setWidth((int)column.width * scaling_factor);
0761 
0762         // plot designation
0763         switch (column.type) {
0764         case Origin::SpreadColumn::X:
0765             col->setPlotDesignation(AbstractColumn::PlotDesignation::X);
0766             break;
0767         case Origin::SpreadColumn::Y:
0768             col->setPlotDesignation(AbstractColumn::PlotDesignation::Y);
0769             break;
0770         case Origin::SpreadColumn::Z:
0771             col->setPlotDesignation(AbstractColumn::PlotDesignation::Z);
0772             break;
0773         case Origin::SpreadColumn::XErr:
0774             col->setPlotDesignation(AbstractColumn::PlotDesignation::XError);
0775             break;
0776         case Origin::SpreadColumn::YErr:
0777             col->setPlotDesignation(AbstractColumn::PlotDesignation::YError);
0778             break;
0779         case Origin::SpreadColumn::Label:
0780         case Origin::SpreadColumn::NONE:
0781         default:
0782             col->setPlotDesignation(AbstractColumn::PlotDesignation::NoDesignation);
0783         }
0784 
0785         QString format;
0786         switch (column.valueType) {
0787         case Origin::Numeric: {
0788             for (unsigned int i = column.beginRow; i < column.endRow; ++i) {
0789                 const double value = column.data.at(i).as_double();
0790                 if (value != _ONAN)
0791                     col->setValueAt(i, value);
0792             }
0793 
0794             loadColumnNumericFormat(column, col);
0795             break;
0796         }
0797         case Origin::TextNumeric: {
0798             // A TextNumeric column can contain numeric and string values, there is no equivalent column mode in LabPlot.
0799             //  -> Set the column mode as 'Numeric' or 'Text' depending on the type of first non-empty element in column.
0800             for (unsigned int i = column.beginRow; i < column.endRow; ++i) {
0801                 const Origin::variant value(column.data.at(i));
0802                 if (value.type() == Origin::Variant::V_DOUBLE) {
0803                     if (value.as_double() != _ONAN)
0804                         break;
0805                 } else {
0806                     if (value.as_string() != nullptr) {
0807                         col->setColumnMode(AbstractColumn::ColumnMode::Text);
0808                         break;
0809                     }
0810                 }
0811             }
0812 
0813             if (col->columnMode() == AbstractColumn::ColumnMode::Double) {
0814                 for (unsigned int i = column.beginRow; i < column.endRow; ++i) {
0815                     const double value = column.data.at(i).as_double();
0816                     if (column.data.at(i).type() == Origin::Variant::V_DOUBLE && value != _ONAN)
0817                         col->setValueAt(i, value);
0818                 }
0819                 loadColumnNumericFormat(column, col);
0820             } else {
0821                 for (unsigned int i = column.beginRow; i < column.endRow; ++i) {
0822                     const Origin::variant value(column.data.at(i));
0823                     if (value.type() == Origin::Variant::V_STRING) {
0824                         if (value.as_string() != nullptr)
0825                             col->setTextAt(i, QLatin1String(value.as_string()));
0826                     } else {
0827                         if (value.as_double() != _ONAN)
0828                             col->setTextAt(i, QString::number(value.as_double()));
0829                     }
0830                 }
0831             }
0832             break;
0833         }
0834         case Origin::Text:
0835             col->setColumnMode(AbstractColumn::ColumnMode::Text);
0836             for (int i = 0; i < std::min((int)column.data.size(), rows); ++i)
0837                 col->setTextAt(i, QLatin1String(column.data[i].as_string()));
0838             break;
0839         case Origin::Time: {
0840             switch (column.valueTypeSpecification + 128) {
0841             case Origin::TIME_HH_MM:
0842                 format = QStringLiteral("hh:mm");
0843                 break;
0844             case Origin::TIME_HH:
0845                 format = QStringLiteral("hh");
0846                 break;
0847             case Origin::TIME_HH_MM_SS:
0848                 format = QStringLiteral("hh:mm:ss");
0849                 break;
0850             case Origin::TIME_HH_MM_SS_ZZ:
0851                 format = QStringLiteral("hh:mm:ss.zzz");
0852                 break;
0853             case Origin::TIME_HH_AP:
0854                 format = QStringLiteral("hh ap");
0855                 break;
0856             case Origin::TIME_HH_MM_AP:
0857                 format = QStringLiteral("hh:mm ap");
0858                 break;
0859             case Origin::TIME_MM_SS:
0860                 format = QStringLiteral("mm:ss");
0861                 break;
0862             case Origin::TIME_MM_SS_ZZ:
0863                 format = QStringLiteral("mm:ss.zzz");
0864                 break;
0865             case Origin::TIME_HHMM:
0866                 format = QStringLiteral("hhmm");
0867                 break;
0868             case Origin::TIME_HHMMSS:
0869                 format = QStringLiteral("hhmmss");
0870                 break;
0871             case Origin::TIME_HH_MM_SS_ZZZ:
0872                 format = QStringLiteral("hh:mm:ss.zzz");
0873                 break;
0874             }
0875 
0876             for (int i = 0; i < std::min((int)column.data.size(), rows); ++i)
0877                 col->setValueAt(i, column.data[i].as_double());
0878             col->setColumnMode(AbstractColumn::ColumnMode::DateTime);
0879 
0880             auto* filter = static_cast<DateTime2StringFilter*>(col->outputFilter());
0881             filter->setFormat(format);
0882             break;
0883         }
0884         case Origin::Date: {
0885             switch (column.valueTypeSpecification) {
0886             case Origin::DATE_DD_MM_YYYY:
0887                 format = QStringLiteral("dd/MM/yyyy");
0888                 break;
0889             case Origin::DATE_DD_MM_YYYY_HH_MM:
0890                 format = QStringLiteral("dd/MM/yyyy HH:mm");
0891                 break;
0892             case Origin::DATE_DD_MM_YYYY_HH_MM_SS:
0893                 format = QStringLiteral("dd/MM/yyyy HH:mm:ss");
0894                 break;
0895             case Origin::DATE_DDMMYYYY:
0896             case Origin::DATE_DDMMYYYY_HH_MM:
0897             case Origin::DATE_DDMMYYYY_HH_MM_SS:
0898                 format = QStringLiteral("dd.MM.yyyy");
0899                 break;
0900             case Origin::DATE_MMM_D:
0901                 format = QStringLiteral("MMM d");
0902                 break;
0903             case Origin::DATE_M_D:
0904                 format = QStringLiteral("M/d");
0905                 break;
0906             case Origin::DATE_D:
0907                 format = QLatin1Char('d');
0908                 break;
0909             case Origin::DATE_DDD:
0910             case Origin::DATE_DAY_LETTER:
0911                 format = QStringLiteral("ddd");
0912                 break;
0913             case Origin::DATE_YYYY:
0914                 format = QStringLiteral("yyyy");
0915                 break;
0916             case Origin::DATE_YY:
0917                 format = QStringLiteral("yy");
0918                 break;
0919             case Origin::DATE_YYMMDD:
0920             case Origin::DATE_YYMMDD_HH_MM:
0921             case Origin::DATE_YYMMDD_HH_MM_SS:
0922             case Origin::DATE_YYMMDD_HHMM:
0923             case Origin::DATE_YYMMDD_HHMMSS:
0924                 format = QStringLiteral("yyMMdd");
0925                 break;
0926             case Origin::DATE_MMM:
0927             case Origin::DATE_MONTH_LETTER:
0928                 format = QStringLiteral("MMM");
0929                 break;
0930             case Origin::DATE_M_D_YYYY:
0931                 format = QStringLiteral("M-d-yyyy");
0932                 break;
0933             default:
0934                 format = QStringLiteral("dd.MM.yyyy");
0935             }
0936 
0937             for (int i = 0; i < std::min((int)column.data.size(), rows); ++i)
0938                 col->setValueAt(i, column.data[i].as_double());
0939             col->setColumnMode(AbstractColumn::ColumnMode::DateTime);
0940 
0941             auto* filter = static_cast<DateTime2StringFilter*>(col->outputFilter());
0942             filter->setFormat(format);
0943             break;
0944         }
0945         case Origin::Month: {
0946             switch (column.valueTypeSpecification) {
0947             case Origin::MONTH_MMM:
0948                 format = QStringLiteral("MMM");
0949                 break;
0950             case Origin::MONTH_MMMM:
0951                 format = QStringLiteral("MMMM");
0952                 break;
0953             case Origin::MONTH_LETTER:
0954                 format = QLatin1Char('M');
0955                 break;
0956             }
0957 
0958             for (int i = 0; i < std::min((int)column.data.size(), rows); ++i)
0959                 col->setValueAt(i, column.data[i].as_double());
0960             col->setColumnMode(AbstractColumn::ColumnMode::Month);
0961 
0962             auto* filter = static_cast<DateTime2StringFilter*>(col->outputFilter());
0963             filter->setFormat(format);
0964             break;
0965         }
0966         case Origin::Day: {
0967             switch (column.valueTypeSpecification) {
0968             case Origin::DAY_DDD:
0969                 format = QStringLiteral("ddd");
0970                 break;
0971             case Origin::DAY_DDDD:
0972                 format = QStringLiteral("dddd");
0973                 break;
0974             case Origin::DAY_LETTER:
0975                 format = QLatin1Char('d');
0976                 break;
0977             }
0978 
0979             for (int i = 0; i < std::min((int)column.data.size(), rows); ++i)
0980                 col->setValueAt(i, column.data[i].as_double());
0981             col->setColumnMode(AbstractColumn::ColumnMode::Day);
0982 
0983             auto* filter = static_cast<DateTime2StringFilter*>(col->outputFilter());
0984             filter->setFormat(format);
0985             break;
0986         }
0987         case Origin::ColumnHeading:
0988         case Origin::TickIndexedDataset:
0989         case Origin::Categorical:
0990             break;
0991         }
0992     }
0993 
0994     // TODO: "hidden" not supported yet
0995     //  if (spread.hidden || spread.loose)
0996     //      mw->hideWindow(spreadsheet);
0997 
0998     return true;
0999 }
1000 
1001 void OriginProjectParser::loadColumnNumericFormat(const Origin::SpreadColumn& originColumn, Column* column) const {
1002     if (originColumn.numericDisplayType != 0) {
1003         int fi = 0;
1004         switch (originColumn.valueTypeSpecification) {
1005         case Origin::Decimal:
1006             fi = 1;
1007             break;
1008         case Origin::Scientific:
1009             fi = 2;
1010             break;
1011         case Origin::Engineering:
1012         case Origin::DecimalWithMarks:
1013             break;
1014         }
1015 
1016         auto* filter = static_cast<Double2StringFilter*>(column->outputFilter());
1017         filter->setNumericFormat(fi);
1018         filter->setNumDigits(originColumn.decimalPlaces);
1019     }
1020 }
1021 
1022 bool OriginProjectParser::loadMatrixWorkbook(Workbook* workbook, bool preview) {
1023     DEBUG(Q_FUNC_INFO)
1024     // load matrix workbook sheets
1025     const auto& originMatrix = m_originFile->matrix(findMatrixByName(workbook->name()));
1026     for (size_t s = 0; s < originMatrix.sheets.size(); ++s) {
1027         auto* matrix = new Matrix(QString::fromStdString(originMatrix.sheets[s].name));
1028         loadMatrix(matrix, preview, s, workbook->name());
1029         workbook->addChildFast(matrix);
1030     }
1031 
1032     return true;
1033 }
1034 
1035 bool OriginProjectParser::loadMatrix(Matrix* matrix, bool preview, size_t sheetIndex, const QString& mwbName) {
1036     DEBUG(Q_FUNC_INFO)
1037     // import matrix data
1038     const auto& originMatrix = m_originFile->matrix(findMatrixByName(mwbName));
1039 
1040     if (preview)
1041         return true;
1042 
1043     // in Origin column width is measured in characters, we need to convert to pixels
1044     // TODO: determine the font used in Origin in order to get the same column width as in Origin
1045     QFont font;
1046     QFontMetrics fm(font);
1047     const int scaling_factor = fm.maxWidth();
1048 
1049     const auto& layer = originMatrix.sheets.at(sheetIndex);
1050     const int colCount = layer.columnCount;
1051     const int rowCount = layer.rowCount;
1052 
1053     matrix->setRowCount(rowCount);
1054     matrix->setColumnCount(colCount);
1055     matrix->setFormula(QString::fromStdString(layer.command));
1056 
1057     // TODO: how to handle different widths for different columns?
1058     for (int j = 0; j < colCount; j++)
1059         matrix->setColumnWidth(j, layer.width * scaling_factor);
1060 
1061     // TODO: check column major vs. row major to improve the performance here
1062     for (int i = 0; i < rowCount; i++) {
1063         for (int j = 0; j < colCount; j++)
1064             matrix->setCell(i, j, layer.data[j + i * colCount]);
1065     }
1066 
1067     char format = 'g';
1068     // TODO: prec not support by Matrix
1069     // int prec = 6;
1070     switch (layer.valueTypeSpecification) {
1071     case 0: // Decimal 1000
1072         format = 'f';
1073         //  prec = layer.decimalPlaces;
1074         break;
1075     case 1: // Scientific
1076         format = 'e';
1077         //  prec = layer.decimalPlaces;
1078         break;
1079     case 2: // Engineering
1080     case 3: // Decimal 1,000
1081         format = 'g';
1082         //  prec = layer.significantDigits;
1083         break;
1084     }
1085 
1086     matrix->setNumericFormat(format);
1087 
1088     return true;
1089 }
1090 
1091 bool OriginProjectParser::loadWorksheet(Worksheet* worksheet, bool preview) {
1092     DEBUG(Q_FUNC_INFO << ", preview = " << preview)
1093     if (worksheet->parentAspect())
1094         DEBUG(Q_FUNC_INFO << ", parent PATH " << STDSTRING(worksheet->parentAspect()->path()))
1095 
1096     // load worksheet data
1097     const auto& graph = m_originFile->graph(findWorksheetByName(worksheet->name()));
1098     DEBUG(Q_FUNC_INFO << ", worksheet name = " << graph.name);
1099     worksheet->setComment(QLatin1String(graph.label.c_str()));
1100 
1101     // TODO: width, height, view mode (print view, page view, window view, draft view)
1102     // Origin allows to freely resize the window and ajusts the size of the plot (layer) automatically
1103     // by keeping a certain width-to-height ratio. It's not clear what the actual size of the plot/layer is and how to handle this.
1104     // For now we simply create a new worksheet here with it's default size and make it using the whole view size.
1105     // Later we can decide to use one of the following properties:
1106     //  1) Window.frameRect gives Rect-corner coordinates (in pixels) of the Window object
1107     //  2) GraphLayer.clientRect gives Rect-corner coordinates (pixels) of the Layer inside the (printer?) page.
1108     //  3) Graph.width, Graph.height give the (printer?) page size in pixels.
1109     //  const QRectF size(0, 0,
1110     //          Worksheet::convertToSceneUnits(graph.width/600., Worksheet::Inch),
1111     //          Worksheet::convertToSceneUnits(graph.height/600., Worksheet::Inch));
1112     //  worksheet->setPageRect(size);
1113     graphSize.rwidth() = graph.width;
1114     graphSize.rheight() = graph.height;
1115     DEBUG(Q_FUNC_INFO << ", GRAPH width/height (px) = " << graphSize.width() << "/" << graphSize.height())
1116     // Graphic elements in Origin are scaled relative to the dimensions of the page (Format->Page) with 600 DPI (>=9.6), 300 DPI (<9.6)
1117     double dpi = 600.;
1118     if (m_originFile->version() < 9.6)
1119         dpi = 300.;
1120     DEBUG(Q_FUNC_INFO << ", GRAPH width/height (cm) = " << graphSize.width() * GSL_CONST_CGS_INCH / dpi << "/" << graphSize.height() * GSL_CONST_CGS_INCH / dpi)
1121     // Origin scales text and plots with the size of the layer when no fixed size is used (Layer properties->Size)
1122     // so we scale all text and plots with a scaling factor to the whole view height (29.5 cm) used as default
1123     const double fixedHeight = 29.5; // full height [cm]
1124     elementScalingFactor = fixedHeight / (graph.height * GSL_CONST_CGS_INCH / dpi);
1125     // not using the full value for scaling text is better in most cases
1126     textScalingFactor = 1. + (elementScalingFactor - 1.) / 2.;
1127     DEBUG(Q_FUNC_INFO << ", ELEMENT SCALING FACTOR = " << elementScalingFactor)
1128     DEBUG(Q_FUNC_INFO << ", TEXT SCALING FACTOR = " << textScalingFactor)
1129     // default values (1000/1000)
1130     //  DEBUG(Q_FUNC_INFO << ", WORKSHEET width/height = " << worksheet->pageRect().width() << "/" << worksheet->pageRect().height())
1131 
1132     worksheet->setUseViewSize(true);
1133 
1134     QHash<TextLabel*, QSizeF> textLabelPositions;
1135 
1136     // worksheet background color
1137     const Origin::ColorGradientDirection bgColorGradient = graph.windowBackgroundColorGradient;
1138     const Origin::Color bgBaseColor = graph.windowBackgroundColorBase;
1139     const Origin::Color bgEndColor = graph.windowBackgroundColorEnd;
1140     worksheet->background()->setColorStyle(backgroundColorStyle(bgColorGradient));
1141     switch (bgColorGradient) {
1142     case Origin::ColorGradientDirection::NoGradient:
1143     case Origin::ColorGradientDirection::TopLeft:
1144     case Origin::ColorGradientDirection::Left:
1145     case Origin::ColorGradientDirection::BottomLeft:
1146     case Origin::ColorGradientDirection::Top:
1147         worksheet->background()->setFirstColor(color(bgEndColor));
1148         worksheet->background()->setSecondColor(color(bgBaseColor));
1149         break;
1150     case Origin::ColorGradientDirection::Center:
1151         break;
1152     case Origin::ColorGradientDirection::Bottom:
1153     case Origin::ColorGradientDirection::TopRight:
1154     case Origin::ColorGradientDirection::Right:
1155     case Origin::ColorGradientDirection::BottomRight:
1156         worksheet->background()->setFirstColor(color(bgBaseColor));
1157         worksheet->background()->setSecondColor(color(bgEndColor));
1158     }
1159 
1160     // TODO: do we need changes on the worksheet layout?
1161 
1162     // process Origin's graph layers - add new plot areas or new coordinate system in the same plot area, depending on the global setting
1163     // https://www.originlab.com/doc/Origin-Help/MultiLayer-Graph
1164     int layerIndex = 0; // index of the graph layer
1165     CartesianPlot* plot = nullptr;
1166     Origin::Rect layerRect;
1167     for (const auto& layer : graph.layers) {
1168         DEBUG(Q_FUNC_INFO << ", Graph Layer " << layerIndex + 1)
1169         if (layer.is3D()) {
1170             // TODO: add an "UnsupportedAspect" here since we don't support 3D yet
1171             break;
1172         }
1173 
1174         layerRect = layer.clientRect;
1175         // DEBUG(Q_FUNC_INFO << ", layer left/right (px) = " << layerRect.left << "/" << layerRect.right)
1176         // DEBUG(Q_FUNC_INFO << ", layer top/bottom (px) = " << layerRect.top << "/" << layerRect.bottom)
1177         // DEBUG(Q_FUNC_INFO << ", layer width/height (px) = " << layerRect.width() << "/" << layerRect.height())
1178 
1179         // create a new plot if we're
1180         // 1. interpreting every layer as a new plot
1181         // 2. interpreting every layer as a new coordinate system in the same and single plot and no plot was created yet
1182         DEBUG(Q_FUNC_INFO << ", layer as plot area = " << m_graphLayerAsPlotArea)
1183         if (m_graphLayerAsPlotArea || (!m_graphLayerAsPlotArea && !plot)) {
1184             plot = new CartesianPlot(i18n("Plot%1", QString::number(layerIndex + 1)));
1185             worksheet->addChildFast(plot);
1186             plot->setIsLoading(true);
1187         }
1188 
1189         loadGraphLayer(layer, plot, layerIndex, textLabelPositions, preview);
1190         ++layerIndex;
1191     }
1192 
1193     // padding
1194     plot->setSymmetricPadding(false);
1195     int numberOfLayer = layerIndex + 1;
1196     DEBUG(Q_FUNC_INFO << ", number of layer = " << numberOfLayer)
1197     if (numberOfLayer == 1 || !m_graphLayerAsPlotArea) { // use layer clientRect for padding
1198         DEBUG(Q_FUNC_INFO << ", using layer rect for padding")
1199         double aspectRatio = graphSize.width() / graphSize.height();
1200 
1201         double leftPadding = layerRect.left / (double)graphSize.width() * aspectRatio * fixedHeight;
1202         double topPadding = layerRect.top / (double)graphSize.height() * fixedHeight;
1203         double rightPadding = (graphSize.width() - layerRect.right) / (double)graphSize.width() * aspectRatio * fixedHeight;
1204         double bottomPadding = (graphSize.height() - layerRect.bottom) / (double)graphSize.height() * fixedHeight;
1205         plot->setHorizontalPadding(Worksheet::convertToSceneUnits(leftPadding, Worksheet::Unit::Centimeter));
1206         plot->setVerticalPadding(Worksheet::convertToSceneUnits(topPadding, Worksheet::Unit::Centimeter));
1207         plot->setRightPadding(Worksheet::convertToSceneUnits(rightPadding, Worksheet::Unit::Centimeter));
1208         plot->setBottomPadding(Worksheet::convertToSceneUnits(bottomPadding, Worksheet::Unit::Centimeter));
1209     } else {
1210         plot->setHorizontalPadding(plot->horizontalPadding() * elementScalingFactor);
1211         plot->setVerticalPadding(plot->verticalPadding() * elementScalingFactor);
1212         plot->setRightPadding(plot->rightPadding() * elementScalingFactor);
1213         plot->setBottomPadding(plot->bottomPadding() * elementScalingFactor);
1214     }
1215     DEBUG(Q_FUNC_INFO << ", PADDING (H/V) = " << plot->horizontalPadding() << ", " << plot->verticalPadding())
1216     DEBUG(Q_FUNC_INFO << ", PADDING (R/B) = " << plot->rightPadding() << ", " << plot->bottomPadding())
1217 
1218     if (!preview) {
1219         worksheet->updateLayout();
1220 
1221         // worksheet and plots got their sizes,
1222         //-> position all text labels inside the plots correctly by converting
1223         // the relative positions determined above to the absolute values
1224         auto it = textLabelPositions.constBegin();
1225         while (it != textLabelPositions.constEnd()) {
1226             auto* label = it.key();
1227             const auto& ratios = it.value();
1228 
1229             auto position = label->position();
1230             position.point.setX(ratios.width());
1231             position.point.setY(ratios.height());
1232             position.horizontalPosition = WorksheetElement::HorizontalPosition::Relative;
1233             position.verticalPosition = WorksheetElement::VerticalPosition::Relative;
1234             // achor depending on rotation
1235             auto rotation = label->rotationAngle();
1236             auto hAlign = WorksheetElement::HorizontalAlignment::Left;
1237             auto vAlign = WorksheetElement::VerticalAlignment::Top;
1238             if (rotation > 45 && rotation <= 135) // left/bottom
1239                 vAlign = WorksheetElement::VerticalAlignment::Bottom;
1240             else if (rotation > 135 && rotation <= 225) { // right/bottom
1241                 hAlign = WorksheetElement::HorizontalAlignment::Right;
1242                 vAlign = WorksheetElement::VerticalAlignment::Bottom;
1243             } else if (rotation > 225) // right/top
1244                 hAlign = WorksheetElement::HorizontalAlignment::Right;
1245 
1246             label->setHorizontalAlignment(hAlign);
1247             label->setVerticalAlignment(vAlign);
1248 
1249             label->setPosition(position);
1250 
1251             ++it;
1252         }
1253     }
1254 
1255     DEBUG(Q_FUNC_INFO << " DONE");
1256     return true;
1257 }
1258 
1259 void OriginProjectParser::loadGraphLayer(const Origin::GraphLayer& layer,
1260                                          CartesianPlot* plot,
1261                                          int layerIndex,
1262                                          QHash<TextLabel*, QSizeF>& textLabelPositions,
1263                                          bool preview) {
1264     DEBUG(Q_FUNC_INFO << ", NEW GRAPH LAYER")
1265 
1266     // background color
1267     const auto& regColor = layer.backgroundColor;
1268     if (regColor.type == Origin::Color::None)
1269         plot->plotArea()->background()->setOpacity(0);
1270     else
1271         plot->plotArea()->background()->setFirstColor(color(regColor));
1272 
1273     // border
1274     if (layer.borderType == Origin::BorderType::None)
1275         plot->plotArea()->borderLine()->setStyle(Qt::NoPen);
1276     else
1277         plot->plotArea()->borderLine()->setStyle(Qt::SolidLine);
1278 
1279     // ranges
1280     const auto& originXAxis = layer.xAxis;
1281     const auto& originYAxis = layer.yAxis;
1282 
1283     Range<double> xRange(originXAxis.min, originXAxis.max);
1284     Range<double> yRange(originYAxis.min, originYAxis.max);
1285     xRange.setAutoScale(false);
1286     yRange.setAutoScale(false);
1287 
1288     if (m_graphLayerAsPlotArea) { // graph layer is read as a new plot area
1289         // set the ranges for default coordinate system
1290         plot->setRangeDefault(Dimension::X, xRange);
1291         plot->setRangeDefault(Dimension::Y, yRange);
1292     } else { // graph layer is read as a new coordinate system in the same plot area
1293         // create a new coordinate systems and set the ranges for it
1294         if (layerIndex > 0) {
1295             // check if identical range already exists
1296             int selectedXRangeIndex = -1;
1297             for (int i = 0; i < plot->rangeCount(Dimension::X); i++) {
1298                 const auto& range = plot->range(Dimension::X, i);
1299                 if (range == xRange) {
1300                     selectedXRangeIndex = i;
1301                     break;
1302                 }
1303             }
1304             int selectedYRangeIndex = -1;
1305             for (int i = 0; i < plot->rangeCount(Dimension::Y); i++) {
1306                 const auto& range = plot->range(Dimension::Y, i);
1307                 if (range == yRange) {
1308                     selectedYRangeIndex = i;
1309                     break;
1310                 }
1311             }
1312 
1313             if (selectedXRangeIndex < 0) {
1314                 plot->addXRange();
1315                 selectedXRangeIndex = plot->rangeCount(Dimension::X) - 1;
1316             }
1317             if (selectedYRangeIndex < 0) {
1318                 plot->addYRange();
1319                 selectedYRangeIndex = plot->rangeCount(Dimension::Y) - 1;
1320             }
1321 
1322             plot->addCoordinateSystem();
1323             // set ranges for new coordinate system
1324             plot->setCoordinateSystemRangeIndex(layerIndex, Dimension::X, selectedXRangeIndex);
1325             plot->setCoordinateSystemRangeIndex(layerIndex, Dimension::Y, selectedYRangeIndex);
1326         }
1327         plot->setRange(Dimension::X, layerIndex, xRange);
1328         plot->setRange(Dimension::Y, layerIndex, yRange);
1329     }
1330 
1331     // scales
1332     switch (originXAxis.scale) {
1333     case Origin::GraphAxis::Linear:
1334         plot->setXRangeScale(RangeT::Scale::Linear);
1335         break;
1336     case Origin::GraphAxis::Log10:
1337         plot->setXRangeScale(RangeT::Scale::Log10);
1338         break;
1339     case Origin::GraphAxis::Ln:
1340         plot->setXRangeScale(RangeT::Scale::Ln);
1341         break;
1342     case Origin::GraphAxis::Log2:
1343         plot->setXRangeScale(RangeT::Scale::Log2);
1344         break;
1345     case Origin::GraphAxis::Reciprocal:
1346         plot->setXRangeScale(RangeT::Scale::Inverse);
1347         break;
1348     case Origin::GraphAxis::Probability:
1349     case Origin::GraphAxis::Probit:
1350     case Origin::GraphAxis::OffsetReciprocal:
1351     case Origin::GraphAxis::Logit:
1352         // TODO:
1353         plot->setXRangeScale(RangeT::Scale::Linear);
1354         break;
1355     }
1356 
1357     switch (originYAxis.scale) {
1358     case Origin::GraphAxis::Linear:
1359         plot->setYRangeScale(RangeT::Scale::Linear);
1360         break;
1361     case Origin::GraphAxis::Log10:
1362         plot->setYRangeScale(RangeT::Scale::Log10);
1363         break;
1364     case Origin::GraphAxis::Ln:
1365         plot->setYRangeScale(RangeT::Scale::Ln);
1366         break;
1367     case Origin::GraphAxis::Log2:
1368         plot->setYRangeScale(RangeT::Scale::Log2);
1369         break;
1370     case Origin::GraphAxis::Reciprocal:
1371         plot->setYRangeScale(RangeT::Scale::Inverse);
1372         break;
1373     case Origin::GraphAxis::Probability:
1374     case Origin::GraphAxis::Probit:
1375     case Origin::GraphAxis::OffsetReciprocal:
1376     case Origin::GraphAxis::Logit:
1377         // TODO:
1378         plot->setYRangeScale(RangeT::Scale::Linear);
1379         break;
1380     }
1381 
1382     // add legend if available
1383     const auto& originLegend = layer.legend;
1384     if (!originLegend.text.empty()) {
1385         QString legendText;
1386         // not using UTF8! (9.85, TO)
1387         legendText = QString::fromLatin1(originLegend.text.c_str());
1388         DEBUG(Q_FUNC_INFO << ", legend text = \"" << STDSTRING(legendText) << "\"");
1389 
1390         auto* legend = new CartesianPlotLegend(i18n("legend"));
1391 
1392         plot->addLegend(legend);
1393 
1394         // set legend text size
1395         DEBUG(Q_FUNC_INFO << ", legend text size = " << originLegend.fontSize);
1396         auto labelFont = legend->labelFont();
1397         labelFont.setPointSize(Worksheet::convertToSceneUnits(originLegend.fontSize * textScalingFactor, Worksheet::Unit::Point));
1398         legend->setLabelFont(labelFont);
1399 
1400         // Origin's legend uses "\l(...)" or "\L(...)" string to format the legend symbol
1401         //  and "%(...) to format the legend text for each curve
1402         // s. a. https://www.originlab.com/doc/Origin-Help/Legend-ManualControl
1403         // the text before these formatting tags, if available, is interpreted as the legend title
1404 
1405         // search for the first occurrence of the legend symbol substring
1406         int index = legendText.indexOf(QLatin1String("\\l("), 0, Qt::CaseInsensitive);
1407         QString legendTitle;
1408         if (index != -1)
1409             legendTitle = legendText.left(index);
1410         else {
1411             // check legend text
1412             index = legendText.indexOf(QLatin1String("%("));
1413             if (index != -1)
1414                 legendTitle = legendText.left(index);
1415         }
1416 
1417         legendTitle = legendTitle.trimmed();
1418         if (!legendTitle.isEmpty())
1419             legendTitle = parseOriginText(legendTitle);
1420         if (!legendTitle.isEmpty()) {
1421             DEBUG(Q_FUNC_INFO << ", legend title = \"" << STDSTRING(legendTitle) << "\"");
1422             legend->title()->setText(legendTitle);
1423         } else {
1424             DEBUG(Q_FUNC_INFO << ", legend title is empty");
1425         }
1426 
1427         // TODO: text color
1428         // const Origin::Color& originColor = originLegend.color;
1429 
1430         // position
1431         // TODO: In Origin the legend can be placed outside of the plot which is not possible in LabPlot.
1432         // To achieve this we'll need to increase padding area in the plot to place the legend outside of the plot area.
1433         // graphSize (% of page), layer.clientRect (% of layer) -> see loadWorksheet()
1434         auto legendRect = originLegend.clientRect;
1435         // auto layerRect = layer.clientRect; // for % of layer
1436         DEBUG(Q_FUNC_INFO << ", LEGEND position (l/t) << " << legendRect.left << "/" << legendRect.top)
1437         DEBUG(Q_FUNC_INFO << ", page size = " << graphSize.width() << "/" << graphSize.height())
1438         CartesianPlotLegend::PositionWrapper position;
1439         QSizeF relativePosition(legendRect.left / (double)graphSize.width(), legendRect.top / (double)graphSize.height());
1440         // achor depending on rotation
1441         auto rotation = originLegend.rotation;
1442         if (rotation > 45 && rotation <= 135) // left/bottom
1443             relativePosition.setHeight(legendRect.bottom / (double)graphSize.height());
1444         else if (rotation > 135 && rotation <= 225) { // right/bottom
1445             relativePosition.setWidth(legendRect.right / (double)graphSize.width());
1446             relativePosition.setHeight(legendRect.bottom / (double)graphSize.height());
1447         } else if (rotation > 225) // right/top
1448             relativePosition.setWidth(legendRect.right / (double)graphSize.width());
1449 
1450         DEBUG(Q_FUNC_INFO << ", relative position to page = " << relativePosition.width() << "/" << relativePosition.height())
1451 
1452         position.point.setX(relativePosition.width());
1453         position.point.setY(relativePosition.height());
1454         // achor depending on rotation
1455         position.horizontalPosition = WorksheetElement::HorizontalPosition::Relative;
1456         position.verticalPosition = WorksheetElement::VerticalPosition::Relative;
1457         auto hAlign = WorksheetElement::HorizontalAlignment::Left;
1458         auto vAlign = WorksheetElement::VerticalAlignment::Top;
1459         if (rotation > 45 && rotation <= 135) // left/bottom
1460             vAlign = WorksheetElement::VerticalAlignment::Bottom;
1461         else if (rotation > 135 && rotation <= 225) { // right/bottom
1462             hAlign = WorksheetElement::HorizontalAlignment::Right;
1463             vAlign = WorksheetElement::VerticalAlignment::Bottom;
1464         } else if (rotation > 225) // right/top
1465             hAlign = WorksheetElement::HorizontalAlignment::Right;
1466         legend->setHorizontalAlignment(hAlign);
1467         legend->setVerticalAlignment(vAlign);
1468 
1469         legend->setPosition(position);
1470 
1471         // rotation
1472         legend->setRotationAngle(originLegend.rotation);
1473 
1474         // border line
1475         if (originLegend.borderType == Origin::BorderType::None)
1476             legend->borderLine()->setStyle(Qt::NoPen);
1477         else
1478             legend->borderLine()->setStyle(Qt::SolidLine);
1479 
1480         // background color, determine it with the help of the border type
1481         if (originLegend.borderType == Origin::BorderType::DarkMarble)
1482             legend->background()->setFirstColor(Qt::darkGray);
1483         else if (originLegend.borderType == Origin::BorderType::BlackOut)
1484             legend->background()->setFirstColor(Qt::black);
1485         else
1486             legend->background()->setFirstColor(Qt::white);
1487 
1488         // save current legend text
1489         m_legendText = legendText;
1490     }
1491 
1492     // curves
1493     loadCurves(layer, plot, layerIndex, preview);
1494 
1495     // reading of other properties is not relevant for the dependency checks in the preview, skip them
1496     if (preview)
1497         return;
1498 
1499     // texts
1500     for (const auto& t : layer.texts) {
1501         DEBUG(Q_FUNC_INFO << ", EXTRA TEXT = " << t.text);
1502         auto* label = new TextLabel(QStringLiteral("text label"));
1503         QString text;
1504         if (m_originFile->version() < 9.5) // <= 2017 : pre-Unicode
1505             text = QString::fromLatin1(t.text.c_str());
1506         else
1507             text = QString::fromStdString(t.text);
1508         QTextEdit te(parseOriginText(text));
1509         te.selectAll();
1510         DEBUG(Q_FUNC_INFO << ", font size = " << t.fontSize)
1511         te.setFontPointSize(int(t.fontSize)); // no scaling
1512         te.setTextColor(OriginProjectParser::color(t.color));
1513         label->setText(te.toHtml());
1514         // DEBUG(" TEXT = " << STDSTRING(label->text().text))
1515 
1516         plot->addChild(label);
1517         label->setParentGraphicsItem(plot->graphicsItem());
1518 
1519         // position
1520         // determine the relative position to the graph
1521         QSizeF relativePosition(t.clientRect.left / (double)graphSize.width(), t.clientRect.top / (double)graphSize.height());
1522         // achor depending on rotation
1523         if (t.rotation > 45 && t.rotation <= 135) // left/bottom
1524             relativePosition.setHeight(t.clientRect.bottom / (double)graphSize.height());
1525         else if (t.rotation > 135 && t.rotation <= 225) { // right/bottom
1526             relativePosition.setWidth(t.clientRect.right / (double)graphSize.width());
1527             relativePosition.setHeight(t.clientRect.bottom / (double)graphSize.height());
1528         } else if (t.rotation > 225) // right/top
1529             relativePosition.setWidth(t.clientRect.right / (double)graphSize.height());
1530 
1531         DEBUG(Q_FUNC_INFO << ", relative position to page = " << relativePosition.width() << "/" << relativePosition.height())
1532         textLabelPositions[label] = relativePosition;
1533 
1534         // rotation
1535         label->setRotationAngle(t.rotation);
1536 
1537         // TODO:
1538         //              int tab;
1539         //              BorderType borderType;
1540         //              Attach attach;
1541     }
1542 
1543     // axes
1544     DEBUG(Q_FUNC_INFO << ", layer.curves.size() = " << layer.curves.size())
1545     if (layer.curves.empty()) // no curves, just axes
1546         loadAxes(layer, plot, layerIndex, QLatin1String("X Axis Title"), QLatin1String("Y Axis Title"));
1547     else {
1548         auto originCurve = layer.curves.at(0);
1549         // see loadCurves()
1550         QString dataName(QString::fromStdString(originCurve.dataName));
1551         DEBUG(Q_FUNC_INFO << ", curve data name " << STDSTRING(dataName))
1552         QString containerName = dataName.right(dataName.length() - 2); // strip "E_" or "T_"
1553         auto sheet = getSpreadsheetByName(containerName);
1554 
1555         QString xColumnName;
1556         if (m_originFile->version() < 9.5) // <= 2017 : pre-Unicode
1557             xColumnName = QString::fromLatin1(originCurve.xColumnName.c_str());
1558         else
1559             xColumnName = QString::fromStdString(originCurve.xColumnName);
1560 
1561         auto xColumn = sheet.columns[findColumnByName(sheet, xColumnName)];
1562         QString xColumnInfo = xColumnName;
1563         if (xColumn.comment.length() > 0) { // long name(, unit(, comment))
1564             if (m_originFile->version() < 9.5) // <= 2017 : pre-Unicode
1565                 xColumnInfo = QString::fromLatin1(xColumn.comment.c_str());
1566             else
1567                 xColumnInfo = QString::fromStdString(xColumn.comment);
1568             if (xColumnInfo.contains(QLatin1Char('@'))) // remove @ options
1569                 xColumnInfo.truncate(xColumnInfo.indexOf(QLatin1Char('@')));
1570         }
1571         DEBUG(Q_FUNC_INFO << ", x column name = " << STDSTRING(xColumnName));
1572         DEBUG(Q_FUNC_INFO << ", x column info = " << STDSTRING(xColumnInfo));
1573 
1574         // same for y
1575         QString yColumnName;
1576         if (m_originFile->version() < 9.5) // <= 2017 : pre-Unicode
1577             yColumnName = QString::fromLatin1(originCurve.yColumnName.c_str());
1578         else
1579             yColumnName = QString::fromStdString(originCurve.yColumnName);
1580 
1581         auto yColumn = sheet.columns[findColumnByName(sheet, yColumnName)];
1582         QString yColumnInfo = yColumnName;
1583         if (yColumn.comment.length() > 0) { // long name(, unit(, comment))
1584             if (m_originFile->version() < 9.5) // <= 2017 : pre-Unicode
1585                 yColumnInfo = QString::fromLatin1(yColumn.comment.c_str());
1586             else
1587                 yColumnInfo = QString::fromStdString(yColumn.comment);
1588             if (yColumnInfo.contains(QLatin1Char('@'))) // remove @ options
1589                 yColumnInfo.truncate(yColumnInfo.indexOf(QLatin1Char('@')));
1590         }
1591         DEBUG(Q_FUNC_INFO << ", y column name = " << STDSTRING(yColumnName));
1592         DEBUG(Q_FUNC_INFO << ", y column info = " << STDSTRING(yColumnInfo));
1593 
1594         loadAxes(layer, plot, layerIndex, xColumnInfo, yColumnInfo);
1595     }
1596 
1597     // range breaks
1598     // TODO
1599 }
1600 
1601 void OriginProjectParser::loadCurves(const Origin::GraphLayer& layer, CartesianPlot* plot, int layerIndex, bool preview) {
1602     DEBUG(Q_FUNC_INFO)
1603 
1604     int curveIndex = 1;
1605     for (const auto& originCurve : layer.curves) {
1606         QString dataName(QString::fromStdString(originCurve.dataName));
1607         DEBUG(Q_FUNC_INFO << ", NEW CURVE (curve data name) " << STDSTRING(dataName))
1608         DEBUG(Q_FUNC_INFO << ", curve x column name = " << originCurve.xColumnName)
1609         DEBUG(Q_FUNC_INFO << ", curve y column name = " << originCurve.yColumnName)
1610         DEBUG(Q_FUNC_INFO << ", curve x data name = " << originCurve.xDataName)
1611 
1612         switch (dataName.at(0).toLatin1()) {
1613         case 'T': // Spreadsheet
1614         case 'E': { // Workbook
1615             if (originCurve.type == Origin::GraphCurve::Line || originCurve.type == Origin::GraphCurve::Scatter
1616                 || originCurve.type == Origin::GraphCurve::LineSymbol || originCurve.type == Origin::GraphCurve::ErrorBar
1617                 || originCurve.type == Origin::GraphCurve::XErrorBar) {
1618                 auto containerName = dataName.right(dataName.length() - 2); // strip "E_" or "T_"
1619                 auto sheet = getSpreadsheetByName(containerName);
1620 
1621                 auto columnName(QString::fromStdString(originCurve.yColumnName));
1622                 auto column = sheet.columns[findColumnByName(sheet, columnName)];
1623                 QString shortName = columnName, curveName = columnName;
1624                 QString longName, unit, comments;
1625                 if (column.comment.length() > 0) {
1626                     auto columnInfo = QString::fromStdString(column.comment); // long name(, unit(, comment))
1627                     DEBUG(Q_FUNC_INFO << ", y column full comment = \"" << column.comment << "\"")
1628                     if (columnInfo.contains(QLatin1Char('@'))) // remove @ options
1629                         columnInfo.truncate(columnInfo.indexOf(QLatin1Char('@')));
1630 
1631                     auto infoList = columnInfo.split(QRegularExpression(QLatin1String("[\r\n]")), Qt::SkipEmptyParts);
1632 
1633                     switch (infoList.size()) {
1634                     case 2: // long name, unit
1635                         unit = infoList.at(1);
1636                         // fallthrough
1637                     case 1: // long name
1638                         longName = infoList.at(0);
1639                         curveName = longName;
1640                         break;
1641                     default: // long name, unit, comment
1642                         longName = infoList.at(0);
1643                         unit = infoList.at(1);
1644                         comments = infoList.at(2);
1645                         curveName = comments;
1646                     }
1647                 }
1648                 if (longName.isEmpty())
1649                     longName = shortName;
1650                 if (comments.isEmpty())
1651                     comments = longName;
1652                 DEBUG(Q_FUNC_INFO << ", default curve name = \"" << curveName.toStdString() << "\"")
1653 
1654                 // TODO: custom legend not used yet
1655                 // Origin's legend uses "%(...) to format the legend text for each curve
1656                 // s. a. https://www.originlab.com/doc/Origin-Help/Legend-ManualControl
1657 
1658                 // parse and use legend text (not used)
1659                 // find substring between %c{curveIndex} and %c{curveIndex+1}
1660                 // int pos1 = legendText.indexOf(QStringLiteral("\\c{%1}").arg(curveIndex)) + 5;
1661                 // int pos2 = legendText.indexOf(QStringLiteral("\\c{%1}").arg(curveIndex + 1));
1662                 // QString curveText = legendText.mid(pos1, pos2 - pos1);
1663                 // replace %(1), %(2), etc. with curve name
1664                 // curveText.replace(QStringLiteral("%(%1)").arg(curveIndex), legendText);
1665 
1666                 // curveText = curveText.trimmed();
1667                 // DEBUG(" curve " << curveIndex << " text = \"" << STDSTRING(curveText) << "\"");
1668                 // TODO: curve (legend) does not support HTML text yet.
1669                 // auto* curve = new XYCurve(curveText);
1670 
1671                 // check if curve is in actual legendText
1672                 // examples:  \l(1) %(1), \l(2) %(2) text, \l(3) %(3,@LG), ..
1673                 DEBUG(Q_FUNC_INFO << ", LEGEND TEXT = \"" << m_legendText.toStdString() << "\"")
1674                 // DEBUG(Q_FUNC_INFO << ", layer index = " << layerIndex + 1 << ", curve index = " << curveIndex)
1675                 bool enableCurveInLegend = false;
1676                 QString legendCurveString;
1677                 // find \l(C)
1678                 int pos1 = m_legendText.indexOf(QStringLiteral("\\l(%1)").arg(curveIndex));
1679                 if (pos1 == -1) // try \l(L.C)
1680                     pos1 = m_legendText.indexOf(QStringLiteral("\\l(%1.%2)").arg(layerIndex + 1).arg(curveIndex));
1681                 else // remove symbol string
1682                     m_legendText.remove(QStringLiteral("\\l(%1)").arg(curveIndex));
1683 
1684                 if (pos1 != -1) { // \l(C) or \l(L.C) found
1685                     // remove symbol string
1686                     m_legendText.remove(QStringLiteral("\\l(%1.%2)").arg(layerIndex + 1).arg(curveIndex));
1687 
1688                     // whole line
1689                     int pos2 = m_legendText.indexOf(QRegularExpression(QLatin1String("[\r\n]")), pos1);
1690                     if (pos2 == -1)
1691                         legendCurveString = m_legendText.mid(pos1, pos2);
1692                     else
1693                         legendCurveString = m_legendText.mid(pos1, pos2 - pos1);
1694                     DEBUG(Q_FUNC_INFO << ", legend curve string = \"" << legendCurveString.toStdString() << "\"")
1695                     if (!legendCurveString.isEmpty()) { // don't include empty entries
1696                         // replace %(C) and %(L.C)
1697                         // see https://www.originlab.com/doc/en/LabTalk/ref/Text-Label-Options#Complete_List_of_.40Options
1698                         QString unitString(unit.isEmpty() ? QStringLiteral("") : QStringLiteral(" (") + unit + QStringLiteral(")"));
1699                         // TODO: implement more
1700                         legendCurveString.replace(QStringLiteral("%(%1)").arg(curveIndex), curveName);
1701                         legendCurveString.replace(QStringLiteral("%(%1,@C)").arg(curveIndex), shortName);
1702                         legendCurveString.replace(QStringLiteral("%(%1,@L)").arg(curveIndex), longName);
1703                         legendCurveString.replace(QStringLiteral("%(%1,@LA)").arg(curveIndex), longName);
1704                         legendCurveString.replace(QStringLiteral("%(%1,@LC)").arg(curveIndex), comments);
1705                         legendCurveString.replace(QStringLiteral("%(%1,@LG)").arg(curveIndex), longName + unitString);
1706                         legendCurveString.replace(QStringLiteral("%(%1,@LL)").arg(curveIndex), longName);
1707                         legendCurveString.replace(QStringLiteral("%(%1,@LM)").arg(curveIndex), comments);
1708                         legendCurveString.replace(QStringLiteral("%(%1,@LN)").arg(curveIndex), comments + unitString);
1709                         legendCurveString.replace(QStringLiteral("%(%1,@LS)").arg(curveIndex), shortName);
1710                         legendCurveString.replace(QStringLiteral("%(%1,@LU)").arg(curveIndex), unit);
1711 
1712                         // same with %(L.C)
1713                         legendCurveString.replace(QStringLiteral("%(%1.%2)").arg(layerIndex + 1).arg(curveIndex), curveName);
1714                         legendCurveString.replace(QStringLiteral("%(%1.%2,@C)").arg(layerIndex + 1).arg(curveIndex), shortName);
1715                         legendCurveString.replace(QStringLiteral("%(%1.%2,@L)").arg(layerIndex + 1).arg(curveIndex), longName);
1716                         legendCurveString.replace(QStringLiteral("%(%1.%2,@LA)").arg(layerIndex + 1).arg(curveIndex), longName);
1717                         legendCurveString.replace(QStringLiteral("%(%1.%2,@LC)").arg(layerIndex + 1).arg(curveIndex), comments);
1718                         legendCurveString.replace(QStringLiteral("%(%1.%2,@LG)").arg(layerIndex + 1).arg(curveIndex), longName + unitString);
1719                         legendCurveString.replace(QStringLiteral("%(%1.%2,@LL)").arg(layerIndex + 1).arg(curveIndex), longName);
1720                         legendCurveString.replace(QStringLiteral("%(%1.%2,@LM)").arg(layerIndex + 1).arg(curveIndex), comments);
1721                         legendCurveString.replace(QStringLiteral("%(%1.%2,@LN)").arg(layerIndex + 1).arg(curveIndex), comments + unitString);
1722                         legendCurveString.replace(QStringLiteral("%(%1.%2,@LS)").arg(layerIndex + 1).arg(curveIndex), shortName);
1723                         legendCurveString.replace(QStringLiteral("%(%1.%2,@LU)").arg(layerIndex + 1).arg(curveIndex), unit);
1724 
1725                         DEBUG(Q_FUNC_INFO << ", legend curve string final = \"" << legendCurveString.toStdString() << "\"")
1726 
1727                         legendCurveString = legendCurveString.trimmed(); // remove leading and trailing whitspaces for curve name
1728                         legendCurveString.remove(QRegularExpression(QStringLiteral("\\\\l\\(\\d+\\)"))); // remove left over "\l(X)" (TO)
1729                         if (!legendCurveString.isEmpty())
1730                             curveName = legendCurveString;
1731 
1732                         enableCurveInLegend = true;
1733                     }
1734                 }
1735 
1736                 DEBUG(Q_FUNC_INFO << ", curve name = \"" << curveName.toStdString() << "\", in legend = " << enableCurveInLegend)
1737 
1738                 auto* curve = new XYCurve(curveName);
1739                 if (m_graphLayerAsPlotArea)
1740                     curve->setCoordinateSystemIndex(plot->defaultCoordinateSystemIndex());
1741                 else
1742                     curve->setCoordinateSystemIndex(layerIndex);
1743                 // DEBUG("CURVE path = " << STDSTRING(data))
1744 
1745                 // set column path
1746                 QString tableName = containerName;
1747                 if (dataName.startsWith(QStringLiteral("E_"))) // container is a workbook
1748                     tableName += QLatin1Char('/') + QString::fromStdString(sheet.name);
1749                 curve->setXColumnPath(tableName + QLatin1Char('/') + QString::fromStdString(originCurve.xColumnName));
1750                 curve->setYColumnPath(tableName + QLatin1Char('/') + QString::fromStdString(originCurve.yColumnName));
1751                 DEBUG(Q_FUNC_INFO << ", x/y column path = \"" << STDSTRING(curve->xColumnPath()) << "\" \"" << STDSTRING(curve->yColumnPath()) << "\"")
1752 
1753                 curve->setLegendVisible(enableCurveInLegend);
1754 
1755                 curve->setSuppressRetransform(true);
1756                 if (!preview)
1757                     loadCurve(originCurve, curve);
1758                 plot->addChildFast(curve);
1759                 curve->setSuppressRetransform(false);
1760             } else if (originCurve.type == Origin::GraphCurve::Column) {
1761                 // TODO: vertical bars
1762 
1763             } else if (originCurve.type == Origin::GraphCurve::Bar) {
1764                 // TODO: horizontal bars
1765 
1766             } else if (originCurve.type == Origin::GraphCurve::Histogram) {
1767                 // TODO
1768             }
1769         } break;
1770         case 'F': {
1771             Origin::Function function;
1772             const auto funcIndex = m_originFile->functionIndex(dataName.right(dataName.length() - 2).toStdString());
1773             if (funcIndex < 0) {
1774                 ++curveIndex;
1775                 continue;
1776             }
1777 
1778             function = m_originFile->function(funcIndex);
1779 
1780             auto* xyEqCurve = new XYEquationCurve(QString::fromStdString(function.name));
1781             if (m_graphLayerAsPlotArea)
1782                 xyEqCurve->setCoordinateSystemIndex(plot->defaultCoordinateSystemIndex());
1783             else
1784                 xyEqCurve->setCoordinateSystemIndex(layerIndex);
1785             XYEquationCurve::EquationData eqData;
1786 
1787             eqData.count = function.totalPoints;
1788             eqData.expression1 = QString::fromStdString(function.formula);
1789 
1790             if (function.type == Origin::Function::Polar) {
1791                 eqData.type = XYEquationCurve::EquationType::Polar;
1792 
1793                 // replace 'x' by 'phi'
1794                 eqData.expression1 = eqData.expression1.replace(QLatin1Char('x'), QLatin1String("phi"));
1795 
1796                 // convert from degrees to radians
1797                 eqData.min = QString::number(function.begin / 180.) + QLatin1String("*pi");
1798                 eqData.max = QString::number(function.end / 180.) + QLatin1String("*pi");
1799             } else {
1800                 eqData.expression1 = QLatin1String(function.formula.c_str());
1801                 eqData.min = QString::number(function.begin);
1802                 eqData.max = QString::number(function.end);
1803             }
1804 
1805             xyEqCurve->setSuppressRetransform(true);
1806             xyEqCurve->setEquationData(eqData);
1807             if (!preview)
1808                 loadCurve(originCurve, xyEqCurve);
1809             plot->addChildFast(xyEqCurve);
1810             xyEqCurve->setSuppressRetransform(false);
1811         }
1812             // TODO case 'M': Matrix
1813         }
1814 
1815         ++curveIndex;
1816     }
1817 }
1818 
1819 void OriginProjectParser::loadAxes(const Origin::GraphLayer& layer,
1820                                    CartesianPlot* plot,
1821                                    int layerIndex,
1822                                    const QString& xColumnInfo,
1823                                    const QString& yColumnInfo) {
1824     const auto& originXAxis = layer.xAxis;
1825     const auto& originYAxis = layer.yAxis;
1826 
1827     // x bottom
1828     if (!originXAxis.formatAxis[0].hidden || originXAxis.tickAxis[0].showMajorLabels) {
1829         auto* axis = new Axis(QStringLiteral("x"), Axis::Orientation::Horizontal);
1830         axis->setSuppressRetransform(true);
1831         axis->setPosition(Axis::Position::Bottom);
1832         plot->addChildFast(axis);
1833 
1834         // fix padding if label not shown
1835         const auto& axisFormat = originXAxis.formatAxis[0];
1836         if (!axisFormat.label.shown)
1837             plot->setBottomPadding(plot->bottomPadding() / 2.);
1838 
1839         loadAxis(originXAxis, axis, layerIndex, 0, xColumnInfo);
1840         if (!m_graphLayerAsPlotArea)
1841             axis->setCoordinateSystemIndex(layerIndex);
1842         axis->setSuppressRetransform(false);
1843     }
1844 
1845     // x top
1846     if (!originXAxis.formatAxis[1].hidden || originXAxis.tickAxis[1].showMajorLabels) {
1847         auto* axis = new Axis(QStringLiteral("x top"), Axis::Orientation::Horizontal);
1848         axis->setPosition(Axis::Position::Top);
1849         axis->setSuppressRetransform(true);
1850         plot->addChildFast(axis);
1851 
1852         // fix padding if label not shown
1853         const auto& axisFormat = originXAxis.formatAxis[1];
1854         if (!axisFormat.label.shown)
1855             plot->setVerticalPadding(plot->verticalPadding() / 2.);
1856 
1857         loadAxis(originXAxis, axis, layerIndex, 1, xColumnInfo);
1858         if (!m_graphLayerAsPlotArea)
1859             axis->setCoordinateSystemIndex(layerIndex);
1860         axis->setSuppressRetransform(false);
1861     }
1862 
1863     // y left
1864     if (!originYAxis.formatAxis[0].hidden || originYAxis.tickAxis[0].showMajorLabels) {
1865         auto* axis = new Axis(QStringLiteral("y"), Axis::Orientation::Vertical);
1866         axis->setSuppressRetransform(true);
1867         axis->setPosition(Axis::Position::Left);
1868         plot->addChildFast(axis);
1869 
1870         // fix padding if label not shown
1871         const auto& axisFormat = originYAxis.formatAxis[0];
1872         if (!axisFormat.label.shown)
1873             plot->setHorizontalPadding(plot->horizontalPadding() / 2.);
1874 
1875         loadAxis(originYAxis, axis, layerIndex, 0, yColumnInfo);
1876         if (!m_graphLayerAsPlotArea)
1877             axis->setCoordinateSystemIndex(layerIndex);
1878         axis->setSuppressRetransform(false);
1879     }
1880 
1881     // y right
1882     if (!originYAxis.formatAxis[1].hidden || originYAxis.tickAxis[1].showMajorLabels) {
1883         auto* axis = new Axis(QStringLiteral("y right"), Axis::Orientation::Vertical);
1884         axis->setSuppressRetransform(true);
1885         axis->setPosition(Axis::Position::Right);
1886         plot->addChildFast(axis);
1887 
1888         // fix padding if label not shown
1889         const auto& axisFormat = originYAxis.formatAxis[1];
1890         if (!axisFormat.label.shown)
1891             plot->setRightPadding(plot->rightPadding() / 2.);
1892 
1893         loadAxis(originYAxis, axis, layerIndex, 1, yColumnInfo);
1894         if (!m_graphLayerAsPlotArea)
1895             axis->setCoordinateSystemIndex(layerIndex);
1896         axis->setSuppressRetransform(false);
1897     }
1898 }
1899 
1900 /*
1901  * sets the axis properties (format and ticks) as defined in \c originAxis in \c axis,
1902  * \c index being 0 or 1 for "bottom" and "top" or "left" and "right" for horizontal or vertical axes, respectively.
1903  */
1904 void OriginProjectParser::loadAxis(const Origin::GraphAxis& originAxis, Axis* axis, int layerIndex, int index, const QString& columnInfo) const {
1905     //  int axisPosition;
1906     //      possible values:
1907     //          0: Axis is at default position
1908     //          1: Axis is at (axisPositionValue)% from standard position
1909     //          2: Axis is at (axisPositionValue) position of orthogonal axis
1910     //      double axisPositionValue;
1911 
1912     //      bool zeroLine;
1913     //      bool oppositeLine;
1914 
1915     DEBUG(Q_FUNC_INFO << ", index = " << index)
1916 
1917     // ranges
1918     axis->setRange(originAxis.min, originAxis.max);
1919 
1920     // ticks
1921     axis->setMajorTicksType(Axis::TicksType::Spacing);
1922     DEBUG(Q_FUNC_INFO << ", step = " << originAxis.step)
1923     DEBUG(Q_FUNC_INFO << ", position = " << originAxis.position)
1924     DEBUG(Q_FUNC_INFO << ", anchor = " << originAxis.anchor)
1925     axis->setMajorTicksSpacing(originAxis.step);
1926     // set offset from step and min later, when scaling factor is known
1927     axis->setMinorTicksType(Axis::TicksType::TotalNumber);
1928     axis->setMinorTicksNumber(originAxis.minorTicks);
1929 
1930     // scale
1931     switch (originAxis.scale) {
1932     case Origin::GraphAxis::Linear:
1933         axis->setScale(RangeT::Scale::Linear);
1934         break;
1935     case Origin::GraphAxis::Log10:
1936         axis->setScale(RangeT::Scale::Log10);
1937         break;
1938     case Origin::GraphAxis::Ln:
1939         axis->setScale(RangeT::Scale::Ln);
1940         break;
1941     case Origin::GraphAxis::Log2:
1942         axis->setScale(RangeT::Scale::Log2);
1943         break;
1944     case Origin::GraphAxis::Reciprocal:
1945         axis->setScale(RangeT::Scale::Inverse);
1946         break;
1947     case Origin::GraphAxis::Probability:
1948     case Origin::GraphAxis::Probit:
1949     case Origin::GraphAxis::OffsetReciprocal:
1950     case Origin::GraphAxis::Logit:
1951         // TODO: set if implemented
1952         axis->setScale(RangeT::Scale::Linear);
1953         break;
1954     }
1955 
1956     // major grid
1957     const auto& majorGrid = originAxis.majorGrid;
1958     Qt::PenStyle penStyle(Qt::NoPen);
1959     if (!majorGrid.hidden) {
1960         switch (majorGrid.style) {
1961         case Origin::GraphCurve::Solid:
1962             penStyle = Qt::SolidLine;
1963             break;
1964         case Origin::GraphCurve::Dash:
1965         case Origin::GraphCurve::ShortDash:
1966             penStyle = Qt::DashLine;
1967             break;
1968         case Origin::GraphCurve::Dot:
1969         case Origin::GraphCurve::ShortDot:
1970             penStyle = Qt::DotLine;
1971             break;
1972         case Origin::GraphCurve::DashDot:
1973         case Origin::GraphCurve::ShortDashDot:
1974             penStyle = Qt::DashDotLine;
1975             break;
1976         case Origin::GraphCurve::DashDotDot:
1977             penStyle = Qt::DashDotDotLine;
1978             break;
1979         }
1980     }
1981     axis->majorGridLine()->setStyle(penStyle);
1982 
1983     Origin::Color gridColor;
1984     gridColor.type = Origin::Color::ColorType::Regular;
1985     gridColor.regular = majorGrid.color;
1986     axis->majorGridLine()->setColor(OriginProjectParser::color(gridColor));
1987     axis->majorGridLine()->setWidth(Worksheet::convertToSceneUnits(majorGrid.width, Worksheet::Unit::Point));
1988 
1989     // minor grid
1990     const auto& minorGrid = originAxis.minorGrid;
1991     penStyle = Qt::NoPen;
1992     if (!minorGrid.hidden) {
1993         switch (minorGrid.style) {
1994         case Origin::GraphCurve::Solid:
1995             penStyle = Qt::SolidLine;
1996             break;
1997         case Origin::GraphCurve::Dash:
1998         case Origin::GraphCurve::ShortDash:
1999             penStyle = Qt::DashLine;
2000             break;
2001         case Origin::GraphCurve::Dot:
2002         case Origin::GraphCurve::ShortDot:
2003             penStyle = Qt::DotLine;
2004             break;
2005         case Origin::GraphCurve::DashDot:
2006         case Origin::GraphCurve::ShortDashDot:
2007             penStyle = Qt::DashDotLine;
2008             break;
2009         case Origin::GraphCurve::DashDotDot:
2010             penStyle = Qt::DashDotDotLine;
2011             break;
2012         }
2013     }
2014     axis->minorGridLine()->setStyle(penStyle);
2015 
2016     gridColor.regular = minorGrid.color;
2017     axis->minorGridLine()->setColor(OriginProjectParser::color(gridColor));
2018     axis->minorGridLine()->setWidth(Worksheet::convertToSceneUnits(minorGrid.width, Worksheet::Unit::Point));
2019 
2020     // process Origin::GraphAxisFormat
2021     const auto& axisFormat = originAxis.formatAxis[index];
2022 
2023     Origin::Color color;
2024     color.type = Origin::Color::ColorType::Regular;
2025     color.regular = axisFormat.color;
2026     axis->line()->setColor(OriginProjectParser::color(color));
2027     // DEBUG("AXIS LINE THICKNESS = " << axisFormat.thickness)
2028     double lineThickness = 1.;
2029     if (layerIndex == 0) // axis line thickness is actually only used for first layer in Origin!
2030         lineThickness = axisFormat.thickness;
2031     axis->line()->setWidth(Worksheet::convertToSceneUnits(lineThickness * elementScalingFactor, Worksheet::Unit::Point));
2032 
2033     if (axisFormat.hidden)
2034         axis->line()->setStyle(Qt::NoPen);
2035     // TODO: read line style properties? (solid line, dashed line, etc.)
2036 
2037     axis->setMajorTicksLength(Worksheet::convertToSceneUnits(axisFormat.majorTickLength * elementScalingFactor, Worksheet::Unit::Point));
2038     axis->setMajorTicksDirection((Axis::TicksFlags)axisFormat.majorTicksType);
2039     axis->majorTicksLine()->setStyle(axis->line()->style());
2040     axis->majorTicksLine()->setColor(axis->line()->color());
2041     axis->majorTicksLine()->setWidth(axis->line()->width());
2042     axis->setMinorTicksLength(axis->majorTicksLength() / 2); // minorTicksLength is half of majorTicksLength
2043     axis->setMinorTicksDirection((Axis::TicksFlags)axisFormat.minorTicksType);
2044     axis->minorTicksLine()->setStyle(axis->line()->style());
2045     axis->minorTicksLine()->setColor(axis->line()->color());
2046     axis->minorTicksLine()->setWidth(axis->line()->width());
2047 
2048     // axis title
2049     if (axisFormat.label.shown) {
2050         /*for (int i=0; i<axisFormat.label.text.size(); i++)
2051             printf(" %c ", axisFormat.label.text.at(i));
2052         printf("\n");
2053         for (int i=0; i<axisFormat.label.text.size(); i++)
2054             printf(" %02hhx", axisFormat.label.text.at(i));
2055         printf("\n");
2056         */
2057         QString titleText;
2058         if (m_originFile->version() < 9.5) // <= 2017 : pre-Unicode
2059             titleText = parseOriginText(QString::fromLatin1(axisFormat.label.text.c_str()));
2060         else
2061             titleText = parseOriginText(QString::fromStdString(axisFormat.label.text));
2062         DEBUG(Q_FUNC_INFO << ", axis title string = " << STDSTRING(titleText));
2063         DEBUG(Q_FUNC_INFO << ", column info = " << STDSTRING(columnInfo));
2064 
2065         // long name(, unit(, comment))
2066         // if long name not defined: columnInfo contains column name (s.a.)
2067         auto infoList = columnInfo.split(QRegularExpression(QStringLiteral("[\r\n]")), Qt::SkipEmptyParts);
2068         QString longName, unit, comments;
2069         switch (infoList.size()) {
2070         case 2: // long name, unit
2071             unit = infoList.at(1);
2072             // fallthrough
2073         case 1: // long name
2074             longName = infoList.at(0);
2075             // curveName = longName;
2076             break;
2077         default: // long name, unit, comment
2078             longName = infoList.at(0);
2079             unit = infoList.at(1);
2080             comments = infoList.at(2);
2081             // curveName = comments;
2082         }
2083         if (comments.isEmpty())
2084             comments = longName;
2085 
2086         QString unitString(unit.isEmpty() ? QStringLiteral("") : QStringLiteral(" (") + unit + QStringLiteral(")"));
2087         // TODO: more replacements here using column info (see loadCurves())
2088         titleText.replace(QLatin1String("%(?X)"), longName + unitString);
2089         titleText.replace(QLatin1String("%(?Y)"), longName + unitString);
2090         titleText.replace(QLatin1String("%(?X,@L)"), longName);
2091         titleText.replace(QLatin1String("%(?Y,@L)"), longName);
2092         titleText.replace(QLatin1String("%(?X,@LC)"), comments);
2093         titleText.replace(QLatin1String("%(?Y,@LC)"), comments);
2094         titleText.replace(QLatin1String("%(?X,@LG)"), longName + unitString);
2095         titleText.replace(QLatin1String("%(?Y,@LG)"), longName + unitString);
2096         titleText.replace(QLatin1String("%(?X,@LL)"), longName);
2097         titleText.replace(QLatin1String("%(?Y,@LL)"), longName);
2098         titleText.replace(QLatin1String("%(?X,@LM)"), comments);
2099         titleText.replace(QLatin1String("%(?Y,@LM)"), comments);
2100         titleText.replace(QLatin1String("%(?X,@LN)"), comments + unitString);
2101         titleText.replace(QLatin1String("%(?Y,@LN)"), comments + unitString);
2102         // not available
2103         //      titleText.replace(QLatin1String("%(?X,@LS)"), shortName);
2104         //      titleText.replace(QLatin1String("%(?Y,@LS)"), shortName);
2105         titleText.replace(QLatin1String("%(?X,@LU)"), unit);
2106         titleText.replace(QLatin1String("%(?Y,@LU)"), unit);
2107 
2108         DEBUG(Q_FUNC_INFO << ", axis title final = " << STDSTRING(titleText));
2109 
2110         // use axisFormat.fontSize to override the global font size for the hmtl string
2111         DEBUG(Q_FUNC_INFO << ", axis font size = " << axisFormat.label.fontSize)
2112         QTextEdit te(titleText);
2113         te.selectAll();
2114         te.setFontPointSize(int(axisFormat.label.fontSize * textScalingFactor));
2115         // TODO: parseOriginText() returns html formatted string. What is axisFormat.color used for?
2116         // te.setTextColor(OriginProjectParser::color(t.color));
2117         axis->title()->setText(te.toHtml());
2118 
2119         // axis->title()->setText(titleText);
2120         axis->title()->setRotationAngle(axisFormat.label.rotation);
2121     } else {
2122         axis->title()->setText({});
2123     }
2124 
2125     // handle string factor member in GraphAxisFormat
2126     double scalingFactor = 1.0;
2127     if (!axisFormat.factor.empty()) {
2128         scalingFactor = 1. / std::stod(axisFormat.factor);
2129         DEBUG(Q_FUNC_INFO << ", scaling factor = " << scalingFactor)
2130         axis->setScalingFactor(scalingFactor);
2131         axis->setShowScaleOffset(false); // don't show scale factor
2132     }
2133     // now set axis ticks start offset
2134     double startOffset = nsl_math_ceil_multiple(originAxis.min * scalingFactor, originAxis.step * scalingFactor) - originAxis.min;
2135     DEBUG(Q_FUNC_INFO << ", min = " << originAxis.min << ", step = " << originAxis.step)
2136     DEBUG(Q_FUNC_INFO << ", start offset = " << startOffset)
2137     if (axis->range().contains(startOffset))
2138         axis->setMajorTickStartOffset(startOffset);
2139     else {
2140         DEBUG(Q_FUNC_INFO << ", WARNING: start offset = " << startOffset << " outside of axis range!")
2141         axis->setMajorTickStartOffset(0.);
2142     }
2143 
2144     axis->setLabelsPrefix(QLatin1String(axisFormat.prefix.c_str()));
2145     axis->setLabelsSuffix(QLatin1String(axisFormat.suffix.c_str()));
2146 
2147     // process Origin::GraphAxisTick
2148     const auto& tickAxis = originAxis.tickAxis[index];
2149     if (tickAxis.showMajorLabels) {
2150         color.type = Origin::Color::ColorType::Regular;
2151         color.regular = tickAxis.color;
2152         axis->setLabelsColor(OriginProjectParser::color(color));
2153         if (index == 0) // left
2154             axis->setLabelsPosition(Axis::LabelsPosition::Out);
2155         else // right
2156             axis->setLabelsPosition(Axis::LabelsPosition::In);
2157     } else {
2158         axis->setLabelsPosition(Axis::LabelsPosition::NoLabels);
2159     }
2160 
2161     // TODO: handle ValueType valueType member in GraphAxisTick
2162     // TODO: handle int valueTypeSpecification in GraphAxisTick
2163 
2164     // precision
2165     if (tickAxis.decimalPlaces == -1) {
2166         DEBUG(Q_FUNC_INFO << ", SET auto precision")
2167         axis->setLabelsAutoPrecision(true);
2168     } else {
2169         DEBUG(Q_FUNC_INFO << ", DISABLE auto precision. decimalPlaces = " << tickAxis.decimalPlaces)
2170         axis->setLabelsPrecision(tickAxis.decimalPlaces);
2171         axis->setLabelsAutoPrecision(false);
2172     }
2173 
2174     QFont font;
2175     // TODO: font family?
2176     DEBUG(Q_FUNC_INFO << ", axis tick label font size = " << tickAxis.fontSize)
2177     DEBUG(Q_FUNC_INFO << ", axis tick label font size in points = " << Worksheet::convertToSceneUnits(tickAxis.fontSize, Worksheet::Unit::Point))
2178     font.setPointSize(static_cast<int>(Worksheet::convertToSceneUnits(tickAxis.fontSize * textScalingFactor, Worksheet::Unit::Point)));
2179     font.setBold(tickAxis.fontBold);
2180     axis->setLabelsFont(font);
2181     // TODO: handle string dataName member in GraphAxisTick
2182     // TODO: handle string columnName member in GraphAxisTick
2183     axis->setLabelsRotationAngle(tickAxis.rotation);
2184 }
2185 
2186 void OriginProjectParser::loadCurve(const Origin::GraphCurve& originCurve, XYCurve* curve) const {
2187     DEBUG(Q_FUNC_INFO)
2188     // line properties
2189     auto penStyle(Qt::NoPen);
2190     if (originCurve.type == Origin::GraphCurve::Line || originCurve.type == Origin::GraphCurve::LineSymbol) {
2191         switch (originCurve.lineConnect) {
2192         case Origin::GraphCurve::NoLine:
2193             curve->setLineType(XYCurve::LineType::NoLine);
2194             break;
2195         case Origin::GraphCurve::Straight:
2196             curve->setLineType(XYCurve::LineType::Line);
2197             break;
2198         case Origin::GraphCurve::TwoPointSegment:
2199             curve->setLineType(XYCurve::LineType::Segments2);
2200             break;
2201         case Origin::GraphCurve::ThreePointSegment:
2202             curve->setLineType(XYCurve::LineType::Segments3);
2203             break;
2204         case Origin::GraphCurve::BSpline:
2205         case Origin::GraphCurve::Bezier:
2206         case Origin::GraphCurve::Spline:
2207             curve->setLineType(XYCurve::LineType::SplineCubicNatural);
2208             break;
2209         case Origin::GraphCurve::StepHorizontal:
2210             curve->setLineType(XYCurve::LineType::StartHorizontal);
2211             break;
2212         case Origin::GraphCurve::StepVertical:
2213             curve->setLineType(XYCurve::LineType::StartVertical);
2214             break;
2215         case Origin::GraphCurve::StepHCenter:
2216             curve->setLineType(XYCurve::LineType::MidpointHorizontal);
2217             break;
2218         case Origin::GraphCurve::StepVCenter:
2219             curve->setLineType(XYCurve::LineType::MidpointVertical);
2220             break;
2221         }
2222 
2223         switch (originCurve.lineStyle) {
2224         case Origin::GraphCurve::Solid:
2225             penStyle = Qt::SolidLine;
2226             break;
2227         case Origin::GraphCurve::Dash:
2228         case Origin::GraphCurve::ShortDash:
2229             penStyle = Qt::DashLine;
2230             break;
2231         case Origin::GraphCurve::Dot:
2232         case Origin::GraphCurve::ShortDot:
2233             penStyle = Qt::DotLine;
2234             break;
2235         case Origin::GraphCurve::DashDot:
2236         case Origin::GraphCurve::ShortDashDot:
2237             penStyle = Qt::DashDotLine;
2238             break;
2239         case Origin::GraphCurve::DashDotDot:
2240             penStyle = Qt::DashDotDotLine;
2241             break;
2242         }
2243 
2244         curve->line()->setStyle(penStyle);
2245         auto lineWidth = std::max(originCurve.lineWidth * elementScalingFactor, 1.); // minimum 1 px
2246         curve->line()->setWidth(Worksheet::convertToSceneUnits(lineWidth, Worksheet::Unit::Point));
2247         curve->line()->setColor(color(originCurve.lineColor));
2248         curve->line()->setOpacity(1 - originCurve.lineTransparency / 255);
2249 
2250         // TODO: handle unsigned char boxWidth of Origin::GraphCurve
2251     } else
2252         curve->line()->setStyle(penStyle);
2253 
2254     // symbol properties
2255     auto* symbol = curve->symbol();
2256     DEBUG(Q_FUNC_INFO << ", curve type = " << (unsigned int)originCurve.type)
2257     if (originCurve.type == Origin::GraphCurve::Scatter || originCurve.type == Origin::GraphCurve::LineSymbol) {
2258         // try to map the different symbols, mapping is not exact
2259         symbol->setRotationAngle(0);
2260         switch (originCurve.symbolShape) { // see https://www.originlab.com/doc/Labtalk/Ref/List-of-Symbol-Shapes
2261         case 0: // NoSymbol
2262             symbol->setStyle(Symbol::Style::NoSymbols);
2263             break;
2264         case 1: // Square
2265             switch (originCurve.symbolInterior) {
2266             case 0: // solid
2267             case 1: // open
2268             case 3: // hollow
2269                 symbol->setStyle(Symbol::Style::Square);
2270                 break;
2271             case 2: // dot
2272                 symbol->setStyle(Symbol::Style::SquareDot);
2273                 break;
2274             case 4: // plus
2275                 symbol->setStyle(Symbol::Style::SquarePlus);
2276                 break;
2277             case 5: // X
2278                 symbol->setStyle(Symbol::Style::SquareX);
2279                 break;
2280             case 6: // minus
2281             case 10: // down
2282                 symbol->setStyle(Symbol::Style::SquareHalf);
2283                 break;
2284             case 7: // pipe
2285                 symbol->setStyle(Symbol::Style::SquareHalf);
2286                 symbol->setRotationAngle(90);
2287                 break;
2288             case 8: // up
2289                 symbol->setStyle(Symbol::Style::SquareHalf);
2290                 symbol->setRotationAngle(180);
2291                 break;
2292             case 9: // right
2293                 symbol->setStyle(Symbol::Style::SquareHalf);
2294                 symbol->setRotationAngle(-90);
2295                 break;
2296             case 11: // left
2297                 symbol->setStyle(Symbol::Style::SquareHalf);
2298                 symbol->setRotationAngle(90);
2299                 break;
2300             }
2301             break;
2302         case 2: // Ellipse
2303         case 20: // Sphere
2304             switch (originCurve.symbolInterior) {
2305             case 0: // solid
2306             case 1: // open
2307             case 3: // hollow
2308                 symbol->setStyle(Symbol::Style::Circle);
2309                 break;
2310             case 2: // dot
2311                 symbol->setStyle(Symbol::Style::CircleDot);
2312                 break;
2313             case 4: // plus
2314                 symbol->setStyle(Symbol::Style::CircleX);
2315                 symbol->setRotationAngle(45);
2316                 break;
2317             case 5: // X
2318                 symbol->setStyle(Symbol::Style::CircleX);
2319                 break;
2320             case 6: // minus
2321                 symbol->setStyle(Symbol::Style::CircleHalf);
2322                 symbol->setRotationAngle(90);
2323                 break;
2324             case 7: // pipe
2325             case 11: // left
2326                 symbol->setStyle(Symbol::Style::CircleHalf);
2327                 break;
2328             case 8: // up
2329                 symbol->setStyle(Symbol::Style::CircleHalf);
2330                 symbol->setRotationAngle(90);
2331                 break;
2332             case 9: // right
2333                 symbol->setStyle(Symbol::Style::CircleHalf);
2334                 symbol->setRotationAngle(180);
2335                 break;
2336             case 10: // down
2337                 symbol->setStyle(Symbol::Style::CircleHalf);
2338                 symbol->setRotationAngle(-90);
2339                 break;
2340             }
2341             break;
2342         case 3: // UTriangle
2343             switch (originCurve.symbolInterior) {
2344             case 0: // solid
2345             case 1: // open
2346             case 3: // hollow
2347             case 4: // plus TODO
2348             case 5: // X    TODO
2349                 symbol->setStyle(Symbol::Style::EquilateralTriangle);
2350                 break;
2351             case 2: // dot
2352                 symbol->setStyle(Symbol::Style::TriangleDot);
2353                 break;
2354             case 7: // pipe
2355             case 11: // left
2356                 symbol->setStyle(Symbol::Style::TriangleLine);
2357                 break;
2358             case 6: // minus
2359             case 8: // up
2360                 symbol->setStyle(Symbol::Style::TriangleHalf);
2361                 break;
2362             case 9: // right    TODO
2363                 symbol->setStyle(Symbol::Style::TriangleLine);
2364                 // symbol->setRotationAngle(180);
2365                 break;
2366             case 10: // down    TODO
2367                 symbol->setStyle(Symbol::Style::TriangleHalf);
2368                 // symbol->setRotationAngle(180);
2369                 break;
2370             }
2371             break;
2372         case 4: // DTriangle
2373             switch (originCurve.symbolInterior) {
2374             case 0: // solid
2375             case 1: // open
2376             case 3: // hollow
2377             case 4: // plus TODO
2378             case 5: // X    TODO
2379                 symbol->setStyle(Symbol::Style::EquilateralTriangle);
2380                 symbol->setRotationAngle(180);
2381                 break;
2382             case 2: // dot
2383                 symbol->setStyle(Symbol::Style::TriangleDot);
2384                 symbol->setRotationAngle(180);
2385                 break;
2386             case 7: // pipe
2387             case 11: // left
2388                 symbol->setStyle(Symbol::Style::TriangleLine);
2389                 symbol->setRotationAngle(180);
2390                 break;
2391             case 6: // minus
2392             case 8: // up
2393                 symbol->setStyle(Symbol::Style::TriangleHalf);
2394                 symbol->setRotationAngle(180);
2395                 break;
2396             case 9: // right    TODO
2397                 symbol->setStyle(Symbol::Style::TriangleLine);
2398                 symbol->setRotationAngle(180);
2399                 break;
2400             case 10: // down    TODO
2401                 symbol->setStyle(Symbol::Style::TriangleHalf);
2402                 symbol->setRotationAngle(180);
2403                 break;
2404             }
2405             break;
2406         case 5: // Diamond
2407             symbol->setStyle(Symbol::Style::Diamond);
2408             switch (originCurve.symbolInterior) {
2409             case 0: // solid
2410             case 1: // open
2411             case 3: // hollow
2412                 symbol->setStyle(Symbol::Style::Diamond);
2413                 break;
2414             case 2: // dot
2415                 symbol->setStyle(Symbol::Style::SquareDot);
2416                 symbol->setRotationAngle(45);
2417                 break;
2418             case 4: // plus
2419                 symbol->setStyle(Symbol::Style::SquareX);
2420                 symbol->setRotationAngle(45);
2421                 break;
2422             case 5: // X
2423                 symbol->setStyle(Symbol::Style::SquarePlus);
2424                 symbol->setRotationAngle(45);
2425                 break;
2426             case 6: // minus
2427             case 10: // down
2428                 symbol->setStyle(Symbol::Style::SquareHalf);
2429                 break;
2430             case 7: // pipe
2431                 symbol->setStyle(Symbol::Style::SquareHalf);
2432                 symbol->setRotationAngle(90);
2433                 break;
2434             case 8: // up
2435                 symbol->setStyle(Symbol::Style::SquareHalf);
2436                 symbol->setRotationAngle(180);
2437                 break;
2438             case 9: // right
2439                 symbol->setStyle(Symbol::Style::SquareHalf);
2440                 symbol->setRotationAngle(-90);
2441                 break;
2442             case 11: // left
2443                 symbol->setStyle(Symbol::Style::SquareHalf);
2444                 symbol->setRotationAngle(90);
2445                 break;
2446             }
2447             break;
2448         case 6: // Cross +
2449             symbol->setStyle(Symbol::Style::Cross);
2450             break;
2451         case 7: // Cross x
2452             symbol->setStyle(Symbol::Style::Cross);
2453             symbol->setRotationAngle(45);
2454             break;
2455         case 8: // Snow
2456             symbol->setStyle(Symbol::Style::XPlus);
2457             break;
2458         case 9: // Horizontal -
2459             symbol->setStyle(Symbol::Style::Line);
2460             symbol->setRotationAngle(90);
2461             break;
2462         case 10: // Vertical |
2463             symbol->setStyle(Symbol::Style::Line);
2464             break;
2465         case 15: // LTriangle
2466             switch (originCurve.symbolInterior) {
2467             case 0: // solid
2468             case 1: // open
2469             case 3: // hollow
2470             case 4: // plus TODO
2471             case 5: // X    TODO
2472                 symbol->setStyle(Symbol::Style::EquilateralTriangle);
2473                 symbol->setRotationAngle(-90);
2474                 break;
2475             case 2: // dot
2476                 symbol->setStyle(Symbol::Style::TriangleDot);
2477                 symbol->setRotationAngle(-90);
2478                 break;
2479             case 7: // pipe
2480             case 11: // left
2481                 symbol->setStyle(Symbol::Style::TriangleLine);
2482                 symbol->setRotationAngle(-90);
2483                 break;
2484             case 6: // minus
2485             case 8: // up
2486                 symbol->setStyle(Symbol::Style::TriangleHalf);
2487                 symbol->setRotationAngle(-90);
2488                 break;
2489             case 9: // right    TODO
2490                 symbol->setStyle(Symbol::Style::TriangleLine);
2491                 symbol->setRotationAngle(-90);
2492                 break;
2493             case 10: // down    TODO
2494                 symbol->setStyle(Symbol::Style::TriangleHalf);
2495                 symbol->setRotationAngle(-90);
2496                 break;
2497             }
2498             break;
2499         case 16: // RTriangle
2500             switch (originCurve.symbolInterior) {
2501             case 0: // solid
2502             case 1: // open
2503             case 3: // hollow
2504             case 4: // plus TODO
2505             case 5: // X    TODO
2506                 symbol->setStyle(Symbol::Style::EquilateralTriangle);
2507                 symbol->setRotationAngle(90);
2508                 break;
2509             case 2: // dot
2510                 symbol->setStyle(Symbol::Style::TriangleDot);
2511                 symbol->setRotationAngle(90);
2512                 break;
2513             case 7: // pipe
2514             case 11: // left
2515                 symbol->setStyle(Symbol::Style::TriangleLine);
2516                 symbol->setRotationAngle(90);
2517                 break;
2518             case 6: // minus
2519             case 8: // up
2520                 symbol->setStyle(Symbol::Style::TriangleHalf);
2521                 symbol->setRotationAngle(90);
2522                 break;
2523             case 9: // right    TODO
2524                 symbol->setStyle(Symbol::Style::TriangleLine);
2525                 symbol->setRotationAngle(90);
2526                 break;
2527             case 10: // down    TODO
2528                 symbol->setStyle(Symbol::Style::TriangleHalf);
2529                 symbol->setRotationAngle(90);
2530                 break;
2531             }
2532             break;
2533         case 17: // Hexagon
2534             symbol->setStyle(Symbol::Style::Hexagon);
2535             break;
2536         case 18: // Star
2537             symbol->setStyle(Symbol::Style::Star);
2538             break;
2539         case 19: // Pentagon
2540             symbol->setStyle(Symbol::Style::Pentagon);
2541             break;
2542         default:
2543             symbol->setStyle(Symbol::Style::NoSymbols);
2544         }
2545 
2546         // symbol size
2547         DEBUG(Q_FUNC_INFO << ", symbol size = " << originCurve.symbolSize)
2548         DEBUG(Q_FUNC_INFO << ", symbol size in points = " << Worksheet::convertToSceneUnits(originCurve.symbolSize, Worksheet::Unit::Point))
2549         symbol->setSize(Worksheet::convertToSceneUnits(originCurve.symbolSize * elementScalingFactor, Worksheet::Unit::Point));
2550 
2551         // symbol colors
2552         DEBUG(Q_FUNC_INFO << ", symbol fill color = " << originCurve.symbolFillColor.type << "_"
2553                           << (Origin::Color::RegularColor)originCurve.symbolFillColor.regular)
2554         DEBUG(Q_FUNC_INFO << ", symbol color = " << originCurve.symbolColor.type << "_" << (Origin::Color::RegularColor)originCurve.symbolColor.regular)
2555         // if plot type == line+symbol
2556 
2557         auto brush = symbol->brush();
2558         auto pen = symbol->pen();
2559         DEBUG(Q_FUNC_INFO << ", SYMBOL THICKNESS = " << (int)originCurve.symbolThickness)
2560         // border width (edge thickness in Origin) is given as percentage of the symbol radius
2561         const double borderScaleFactor = 5.; // match size
2562         if (originCurve.type == Origin::GraphCurve::LineSymbol) {
2563             // symbol fill color
2564             if (originCurve.symbolFillColor.type == Origin::Color::ColorType::Automatic) {
2565                 //"automatic" color -> the color of the line, if available, is used, and black otherwise
2566                 if (curve->lineType() != XYCurve::LineType::NoLine)
2567                     brush.setColor(curve->line()->pen().color());
2568                 else
2569                     brush.setColor(Qt::black);
2570             } else
2571                 brush.setColor(color(originCurve.symbolFillColor));
2572             if (originCurve.symbolInterior > 0 && originCurve.symbolInterior < 8) // unfilled styles
2573                 brush.setStyle(Qt::NoBrush);
2574 
2575             // symbol border/edge color and width
2576             if (originCurve.symbolColor.type == Origin::Color::ColorType::Automatic) {
2577                 //"automatic" color -> the color of the line, if available, has to be used, black otherwise
2578                 if (curve->lineType() != XYCurve::LineType::NoLine)
2579                     pen.setColor(curve->line()->pen().color());
2580                 else
2581                     pen.setColor(Qt::black);
2582             } else
2583                 pen.setColor(color(originCurve.symbolColor));
2584 
2585             DEBUG(Q_FUNC_INFO << ", BORDER THICKNESS = " << borderScaleFactor * originCurve.symbolThickness / 100. * symbol->size())
2586             pen.setWidthF(borderScaleFactor * originCurve.symbolThickness / 100. * symbol->size());
2587         } else if (originCurve.type == Origin::GraphCurve::Scatter) {
2588             // symbol color (uses originCurve.symbolColor)
2589             if (originCurve.symbolColor.type == Origin::Color::ColorType::Automatic) {
2590                 //"automatic" color -> the color of the line, if available, is used, and black otherwise
2591                 if (curve->lineType() != XYCurve::LineType::NoLine)
2592                     brush.setColor(curve->line()->pen().color());
2593                 else
2594                     brush.setColor(Qt::black);
2595             } else
2596                 brush.setColor(color(originCurve.symbolColor));
2597 
2598             if (originCurve.symbolInterior > 0 && originCurve.symbolInterior < 8) { // unfilled styles
2599                 brush.setStyle(Qt::NoBrush);
2600                 DEBUG(Q_FUNC_INFO << ", BORDER THICKNESS = " << borderScaleFactor * originCurve.symbolThickness / 100. * symbol->size())
2601                 pen.setWidthF(borderScaleFactor * originCurve.symbolThickness / 100. * symbol->size());
2602 
2603                 // symbol border/edge color and width
2604                 if (originCurve.symbolColor.type == Origin::Color::ColorType::Automatic) {
2605                     //"automatic" color -> the color of the line, if available, has to be used, black otherwise
2606                     if (curve->lineType() != XYCurve::LineType::NoLine)
2607                         pen.setColor(curve->line()->pen().color());
2608                     else
2609                         pen.setColor(Qt::black);
2610                 } else
2611                     pen.setColor(color(originCurve.symbolColor));
2612             } else
2613                 pen.setStyle(Qt::NoPen); // no border
2614         }
2615         symbol->setBrush(brush);
2616         symbol->setPen(pen);
2617 
2618         // handle unsigned char pointOffset member
2619         // handle bool connectSymbols member
2620     } else {
2621         symbol->setStyle(Symbol::Style::NoSymbols);
2622     }
2623 
2624     // filling properties
2625     if (originCurve.fillArea) {
2626         // TODO: handle unsigned char fillAreaType;
2627         // with 'fillAreaType'=0x10 the area between the curve and the x-axis is filled
2628         // with 'fillAreaType'=0x14 the area included inside the curve is filled. First and last curve points are joined by a line to close the otherwise open
2629         // area. with 'fillAreaType'=0x12 the area excluded outside the curve is filled. The inverse of fillAreaType=0x14 is filled. At the moment we only
2630         // support the first type, so set it to XYCurve::FillingBelow
2631         curve->background()->setPosition(Background::Position::Below);
2632         auto* background = curve->background();
2633 
2634         if (originCurve.fillAreaPattern == 0) {
2635             background->setType(Background::Type::Color);
2636         } else {
2637             background->setType(Background::Type::Pattern);
2638 
2639             // map different patterns in originCurve.fillAreaPattern (has the values of Origin::FillPattern) to Qt::BrushStyle;
2640             switch (originCurve.fillAreaPattern) {
2641             case 0:
2642                 background->setBrushStyle(Qt::NoBrush);
2643                 break;
2644             case 1:
2645             case 2:
2646             case 3:
2647                 background->setBrushStyle(Qt::BDiagPattern);
2648                 break;
2649             case 4:
2650             case 5:
2651             case 6:
2652                 background->setBrushStyle(Qt::FDiagPattern);
2653                 break;
2654             case 7:
2655             case 8:
2656             case 9:
2657                 background->setBrushStyle(Qt::DiagCrossPattern);
2658                 break;
2659             case 10:
2660             case 11:
2661             case 12:
2662                 background->setBrushStyle(Qt::HorPattern);
2663                 break;
2664             case 13:
2665             case 14:
2666             case 15:
2667                 background->setBrushStyle(Qt::VerPattern);
2668                 break;
2669             case 16:
2670             case 17:
2671             case 18:
2672                 background->setBrushStyle(Qt::CrossPattern);
2673                 break;
2674             }
2675         }
2676 
2677         background->setFirstColor(color(originCurve.fillAreaColor));
2678         background->setOpacity(1 - originCurve.fillAreaTransparency / 255);
2679 
2680         // Color fillAreaPatternColor - color for the pattern lines, not supported
2681         // double fillAreaPatternWidth - width of the pattern lines, not supported
2682         // bool fillAreaWithLineTransparency - transparency of the pattern lines independent of the area transparency, not supported
2683 
2684         // TODO:
2685         // unsigned char fillAreaPatternBorderStyle;
2686         // Color fillAreaPatternBorderColor;
2687         // double fillAreaPatternBorderWidth;
2688         // The Border properties are used only in "Column/Bar" (histogram) plots. Those properties are:
2689         // fillAreaPatternBorderStyle   for the line style (use enum Origin::LineStyle here)
2690         // fillAreaPatternBorderColor   for the line color
2691         // fillAreaPatternBorderWidth   for the line width
2692     } else
2693         curve->background()->setPosition(Background::Position::No);
2694 }
2695 
2696 bool OriginProjectParser::loadNote(Note* note, bool preview) {
2697     DEBUG(Q_FUNC_INFO);
2698     // load note data
2699     const auto& originNote = m_originFile->note(findNoteByName(note->name()));
2700 
2701     if (preview)
2702         return true;
2703 
2704     note->setComment(QString::fromStdString(originNote.label));
2705     note->setNote(QString::fromStdString(originNote.text));
2706 
2707     return true;
2708 }
2709 
2710 // ##############################################################################
2711 // ########################### Helper functions  ################################
2712 // ##############################################################################
2713 QDateTime OriginProjectParser::creationTime(tree<Origin::ProjectNode>::iterator it) const {
2714     // this logic seems to be correct only for the first node (project node). For other nodes the current time is returned.
2715     char time_str[21];
2716     strftime(time_str, sizeof(time_str), "%F %T", gmtime(&(*it).creationDate));
2717     return QDateTime::fromString(QLatin1String(time_str), Qt::ISODate);
2718 }
2719 
2720 QString OriginProjectParser::parseOriginText(const QString& str) const {
2721     DEBUG(Q_FUNC_INFO);
2722     auto lines = str.split(QLatin1Char('\n'));
2723     QString text;
2724     for (int i = 0; i < lines.size(); ++i) {
2725         if (i > 0)
2726             text.append(QLatin1String("<br>"));
2727         text.append(parseOriginTags(lines[i]));
2728     }
2729 
2730     DEBUG(Q_FUNC_INFO << ", PARSED TEXT = " << STDSTRING(text));
2731 
2732     return text;
2733 }
2734 
2735 QColor OriginProjectParser::color(Origin::Color color) const {
2736     switch (color.type) {
2737     case Origin::Color::ColorType::Regular:
2738         switch (color.regular) {
2739         case Origin::Color::Black:
2740             return QColor{Qt::black};
2741         case Origin::Color::Red:
2742             return QColor{Qt::red};
2743         case Origin::Color::Green:
2744             return QColor{Qt::green};
2745         case Origin::Color::Blue:
2746             return QColor{Qt::blue};
2747         case Origin::Color::Cyan:
2748             return QColor{Qt::cyan};
2749         case Origin::Color::Magenta:
2750             return QColor{Qt::magenta};
2751         case Origin::Color::Yellow:
2752             return QColor{Qt::yellow};
2753         case Origin::Color::DarkYellow:
2754             return QColor{Qt::darkYellow};
2755         case Origin::Color::Navy:
2756             return QColor{0, 0, 128};
2757         case Origin::Color::Purple:
2758             return QColor{128, 0, 128};
2759         case Origin::Color::Wine:
2760             return QColor{128, 0, 0};
2761         case Origin::Color::Olive:
2762             return QColor{0, 128, 0};
2763         case Origin::Color::DarkCyan:
2764             return QColor{Qt::darkCyan};
2765         case Origin::Color::Royal:
2766             return QColor{0, 0, 160};
2767         case Origin::Color::Orange:
2768             return QColor{255, 128, 0};
2769         case Origin::Color::Violet:
2770             return QColor{128, 0, 255};
2771         case Origin::Color::Pink:
2772             return QColor{255, 0, 128};
2773         case Origin::Color::White:
2774             return QColor{Qt::white};
2775         case Origin::Color::LightGray:
2776             return QColor{Qt::lightGray};
2777         case Origin::Color::Gray:
2778             return QColor{Qt::gray};
2779         case Origin::Color::LTYellow:
2780             return QColor{255, 0, 128};
2781         case Origin::Color::LTCyan:
2782             return QColor{128, 255, 255};
2783         case Origin::Color::LTMagenta:
2784             return QColor{255, 128, 255};
2785         case Origin::Color::DarkGray:
2786             return QColor{Qt::darkGray};
2787         case Origin::Color::SpecialV7Axis:
2788             return QColor{Qt::black};
2789         }
2790         break;
2791     case Origin::Color::ColorType::Custom:
2792         return QColor{color.custom[0], color.custom[1], color.custom[2]};
2793     case Origin::Color::ColorType::None:
2794     case Origin::Color::ColorType::Automatic:
2795     case Origin::Color::ColorType::Increment:
2796     case Origin::Color::ColorType::Indexing:
2797     case Origin::Color::ColorType::RGB:
2798     case Origin::Color::ColorType::Mapping:
2799         break;
2800     }
2801 
2802     return Qt::white;
2803 }
2804 
2805 Background::ColorStyle OriginProjectParser::backgroundColorStyle(Origin::ColorGradientDirection colorGradient) const {
2806     switch (colorGradient) {
2807     case Origin::ColorGradientDirection::NoGradient:
2808         return Background::ColorStyle::SingleColor;
2809     case Origin::ColorGradientDirection::TopLeft:
2810         return Background::ColorStyle::TopLeftDiagonalLinearGradient;
2811     case Origin::ColorGradientDirection::Left:
2812         return Background::ColorStyle::HorizontalLinearGradient;
2813     case Origin::ColorGradientDirection::BottomLeft:
2814         return Background::ColorStyle::BottomLeftDiagonalLinearGradient;
2815     case Origin::ColorGradientDirection::Top:
2816         return Background::ColorStyle::VerticalLinearGradient;
2817     case Origin::ColorGradientDirection::Center:
2818         return Background::ColorStyle::RadialGradient;
2819     case Origin::ColorGradientDirection::Bottom:
2820         return Background::ColorStyle::VerticalLinearGradient;
2821     case Origin::ColorGradientDirection::TopRight:
2822         return Background::ColorStyle::BottomLeftDiagonalLinearGradient;
2823     case Origin::ColorGradientDirection::Right:
2824         return Background::ColorStyle::HorizontalLinearGradient;
2825     case Origin::ColorGradientDirection::BottomRight:
2826         return Background::ColorStyle::TopLeftDiagonalLinearGradient;
2827     }
2828 
2829     return Background::ColorStyle::SingleColor;
2830 }
2831 
2832 QString strreverse(const QString& str) { // QString reversing
2833     auto ba = str.toLocal8Bit();
2834     std::reverse(ba.begin(), ba.end());
2835 
2836     return QLatin1String(ba);
2837 }
2838 
2839 QList<QPair<QString, QString>> OriginProjectParser::charReplacementList() const {
2840     QList<QPair<QString, QString>> replacements;
2841 
2842     // TODO: probably missed some. Is there any generic method?
2843     replacements << qMakePair(QStringLiteral("ä"), QStringLiteral("&auml;"));
2844     replacements << qMakePair(QStringLiteral("ö"), QStringLiteral("&ouml;"));
2845     replacements << qMakePair(QStringLiteral("ü"), QStringLiteral("&uuml;"));
2846     replacements << qMakePair(QStringLiteral("Ä"), QStringLiteral("&Auml;"));
2847     replacements << qMakePair(QStringLiteral("Ö"), QStringLiteral("&Ouml;"));
2848     replacements << qMakePair(QStringLiteral("Ü"), QStringLiteral("&Uuml;"));
2849     replacements << qMakePair(QStringLiteral("ß"), QStringLiteral("&szlig;"));
2850     replacements << qMakePair(QStringLiteral("€"), QStringLiteral("&euro;"));
2851     replacements << qMakePair(QStringLiteral("£"), QStringLiteral("&pound;"));
2852     replacements << qMakePair(QStringLiteral("¥"), QStringLiteral("&yen;"));
2853     replacements << qMakePair(QStringLiteral("¤"), QStringLiteral("&curren;")); // krazy:exclude=spelling
2854     replacements << qMakePair(QStringLiteral("¦"), QStringLiteral("&brvbar;"));
2855     replacements << qMakePair(QStringLiteral("§"), QStringLiteral("&sect;"));
2856     replacements << qMakePair(QStringLiteral("µ"), QStringLiteral("&micro;"));
2857     replacements << qMakePair(QStringLiteral("¹"), QStringLiteral("&sup1;"));
2858     replacements << qMakePair(QStringLiteral("²"), QStringLiteral("&sup2;"));
2859     replacements << qMakePair(QStringLiteral("³"), QStringLiteral("&sup3;"));
2860     replacements << qMakePair(QStringLiteral("¶"), QStringLiteral("&para;"));
2861     replacements << qMakePair(QStringLiteral("ø"), QStringLiteral("&oslash;"));
2862     replacements << qMakePair(QStringLiteral("æ"), QStringLiteral("&aelig;"));
2863     replacements << qMakePair(QStringLiteral("ð"), QStringLiteral("&eth;"));
2864     replacements << qMakePair(QStringLiteral("ħ"), QStringLiteral("&hbar;"));
2865     replacements << qMakePair(QStringLiteral("ĸ"), QStringLiteral("&kappa;"));
2866     replacements << qMakePair(QStringLiteral("¢"), QStringLiteral("&cent;"));
2867     replacements << qMakePair(QStringLiteral("¼"), QStringLiteral("&frac14;"));
2868     replacements << qMakePair(QStringLiteral("½"), QStringLiteral("&frac12;"));
2869     replacements << qMakePair(QStringLiteral("¾"), QStringLiteral("&frac34;"));
2870     replacements << qMakePair(QStringLiteral("¬"), QStringLiteral("&not;"));
2871     replacements << qMakePair(QStringLiteral("©"), QStringLiteral("&copy;"));
2872     replacements << qMakePair(QStringLiteral("®"), QStringLiteral("&reg;"));
2873     replacements << qMakePair(QStringLiteral("ª"), QStringLiteral("&ordf;"));
2874     replacements << qMakePair(QStringLiteral("º"), QStringLiteral("&ordm;"));
2875     replacements << qMakePair(QStringLiteral("±"), QStringLiteral("&plusmn;"));
2876     replacements << qMakePair(QStringLiteral("¿"), QStringLiteral("&iquest;"));
2877     replacements << qMakePair(QStringLiteral("×"), QStringLiteral("&times;"));
2878     replacements << qMakePair(QStringLiteral("°"), QStringLiteral("&deg;"));
2879     replacements << qMakePair(QStringLiteral("«"), QStringLiteral("&laquo;"));
2880     replacements << qMakePair(QStringLiteral("»"), QStringLiteral("&raquo;"));
2881     replacements << qMakePair(QStringLiteral("¯"), QStringLiteral("&macr;"));
2882     replacements << qMakePair(QStringLiteral("¸"), QStringLiteral("&cedil;"));
2883     replacements << qMakePair(QStringLiteral("À"), QStringLiteral("&Agrave;"));
2884     replacements << qMakePair(QStringLiteral("Á"), QStringLiteral("&Aacute;"));
2885     replacements << qMakePair(QStringLiteral("Â"), QStringLiteral("&Acirc;"));
2886     replacements << qMakePair(QStringLiteral("Ã"), QStringLiteral("&Atilde;"));
2887     replacements << qMakePair(QStringLiteral("Å"), QStringLiteral("&Aring;"));
2888     replacements << qMakePair(QStringLiteral("Æ"), QStringLiteral("&AElig;"));
2889     replacements << qMakePair(QStringLiteral("Ç"), QStringLiteral("&Ccedil;"));
2890     replacements << qMakePair(QStringLiteral("È"), QStringLiteral("&Egrave;"));
2891     replacements << qMakePair(QStringLiteral("É"), QStringLiteral("&Eacute;"));
2892     replacements << qMakePair(QStringLiteral("Ê"), QStringLiteral("&Ecirc;"));
2893     replacements << qMakePair(QStringLiteral("Ë"), QStringLiteral("&Euml;"));
2894     replacements << qMakePair(QStringLiteral("Ì"), QStringLiteral("&Igrave;"));
2895     replacements << qMakePair(QStringLiteral("Í"), QStringLiteral("&Iacute;"));
2896     replacements << qMakePair(QStringLiteral("Î"), QStringLiteral("&Icirc;"));
2897     replacements << qMakePair(QStringLiteral("Ï"), QStringLiteral("&Iuml;"));
2898     replacements << qMakePair(QStringLiteral("Ð"), QStringLiteral("&ETH;"));
2899     replacements << qMakePair(QStringLiteral("Ñ"), QStringLiteral("&Ntilde;"));
2900     replacements << qMakePair(QStringLiteral("Ò"), QStringLiteral("&Ograve;"));
2901     replacements << qMakePair(QStringLiteral("Ó"), QStringLiteral("&Oacute;"));
2902     replacements << qMakePair(QStringLiteral("Ô"), QStringLiteral("&Ocirc;"));
2903     replacements << qMakePair(QStringLiteral("Õ"), QStringLiteral("&Otilde;"));
2904     replacements << qMakePair(QStringLiteral("Ù"), QStringLiteral("&Ugrave;"));
2905     replacements << qMakePair(QStringLiteral("Ú"), QStringLiteral("&Uacute;"));
2906     replacements << qMakePair(QStringLiteral("Û"), QStringLiteral("&Ucirc;"));
2907     replacements << qMakePair(QStringLiteral("Ý"), QStringLiteral("&Yacute;"));
2908     replacements << qMakePair(QStringLiteral("Þ"), QStringLiteral("&THORN;"));
2909     replacements << qMakePair(QStringLiteral("à"), QStringLiteral("&agrave;"));
2910     replacements << qMakePair(QStringLiteral("á"), QStringLiteral("&aacute;"));
2911     replacements << qMakePair(QStringLiteral("â"), QStringLiteral("&acirc;"));
2912     replacements << qMakePair(QStringLiteral("ã"), QStringLiteral("&atilde;"));
2913     replacements << qMakePair(QStringLiteral("å"), QStringLiteral("&aring;"));
2914     replacements << qMakePair(QStringLiteral("ç"), QStringLiteral("&ccedil;"));
2915     replacements << qMakePair(QStringLiteral("è"), QStringLiteral("&egrave;"));
2916     replacements << qMakePair(QStringLiteral("é"), QStringLiteral("&eacute;"));
2917     replacements << qMakePair(QStringLiteral("ê"), QStringLiteral("&ecirc;"));
2918     replacements << qMakePair(QStringLiteral("ë"), QStringLiteral("&euml;"));
2919     replacements << qMakePair(QStringLiteral("ì"), QStringLiteral("&igrave;"));
2920     replacements << qMakePair(QStringLiteral("í"), QStringLiteral("&iacute;"));
2921     replacements << qMakePair(QStringLiteral("î"), QStringLiteral("&icirc;"));
2922     replacements << qMakePair(QStringLiteral("ï"), QStringLiteral("&iuml;"));
2923     replacements << qMakePair(QStringLiteral("ñ"), QStringLiteral("&ntilde;"));
2924     replacements << qMakePair(QStringLiteral("ò"), QStringLiteral("&ograve;"));
2925     replacements << qMakePair(QStringLiteral("ó"), QStringLiteral("&oacute;"));
2926     replacements << qMakePair(QStringLiteral("ô"), QStringLiteral("&ocirc;"));
2927     replacements << qMakePair(QStringLiteral("õ"), QStringLiteral("&otilde;"));
2928     replacements << qMakePair(QStringLiteral("÷"), QStringLiteral("&divide;"));
2929     replacements << qMakePair(QStringLiteral("ù"), QStringLiteral("&ugrave;"));
2930     replacements << qMakePair(QStringLiteral("ú"), QStringLiteral("&uacute;"));
2931     replacements << qMakePair(QStringLiteral("û"), QStringLiteral("&ucirc;"));
2932     replacements << qMakePair(QStringLiteral("ý"), QStringLiteral("&yacute;"));
2933     replacements << qMakePair(QStringLiteral("þ"), QStringLiteral("&thorn;"));
2934     replacements << qMakePair(QStringLiteral("ÿ"), QStringLiteral("&yuml;"));
2935     replacements << qMakePair(QStringLiteral("Œ"), QStringLiteral("&#338;"));
2936     replacements << qMakePair(QStringLiteral("œ"), QStringLiteral("&#339;"));
2937     replacements << qMakePair(QStringLiteral("Š"), QStringLiteral("&#352;"));
2938     replacements << qMakePair(QStringLiteral("š"), QStringLiteral("&#353;"));
2939     replacements << qMakePair(QStringLiteral("Ÿ"), QStringLiteral("&#376;"));
2940     replacements << qMakePair(QStringLiteral("†"), QStringLiteral("&#8224;"));
2941     replacements << qMakePair(QStringLiteral("‡"), QStringLiteral("&#8225;"));
2942     replacements << qMakePair(QStringLiteral("…"), QStringLiteral("&#8230;"));
2943     replacements << qMakePair(QStringLiteral("‰"), QStringLiteral("&#8240;"));
2944     replacements << qMakePair(QStringLiteral("™"), QStringLiteral("&#8482;"));
2945 
2946     return replacements;
2947 }
2948 
2949 QString OriginProjectParser::replaceSpecialChars(const QString& text) const {
2950     QString t = text;
2951     DEBUG(Q_FUNC_INFO << ", got " << t.toStdString())
2952     for (const auto& r : charReplacementList())
2953         t.replace(r.first, r.second);
2954     DEBUG(Q_FUNC_INFO << ", now " << t.toStdString())
2955     return t;
2956 }
2957 
2958 /*!
2959  * helper function mapping the characters from the Symbol font (outdated and shouldn't be used for html)
2960  * to Unicode characters, s.a. https://www.alanwood.net/demos/symbol.html
2961  */
2962 QString greekSymbol(const QString& symbol) {
2963     // characters in the Symbol-font
2964     static QStringList symbols{// letters
2965                                QStringLiteral("A"),
2966                                QStringLiteral("a"),
2967                                QStringLiteral("B"),
2968                                QStringLiteral("b"),
2969                                QStringLiteral("G"),
2970                                QStringLiteral("g"),
2971                                QStringLiteral("D"),
2972                                QStringLiteral("d"),
2973                                QStringLiteral("E"),
2974                                QStringLiteral("e"),
2975                                QStringLiteral("Z"),
2976                                QStringLiteral("z"),
2977                                QStringLiteral("H"),
2978                                QStringLiteral("h"),
2979                                QStringLiteral("Q"),
2980                                QStringLiteral("q"),
2981                                QStringLiteral("I"),
2982                                QStringLiteral("i"),
2983                                QStringLiteral("K"),
2984                                QStringLiteral("k"),
2985                                QStringLiteral("L"),
2986                                QStringLiteral("l"),
2987                                QStringLiteral("M"),
2988                                QStringLiteral("m"),
2989                                QStringLiteral("N"),
2990                                QStringLiteral("n"),
2991                                QStringLiteral("X"),
2992                                QStringLiteral("x"),
2993                                QStringLiteral("O"),
2994                                QStringLiteral("o"),
2995                                QStringLiteral("P"),
2996                                QStringLiteral("p"),
2997                                QStringLiteral("R"),
2998                                QStringLiteral("r"),
2999                                QStringLiteral("S"),
3000                                QStringLiteral("s"),
3001                                QStringLiteral("T"),
3002                                QStringLiteral("t"),
3003                                QStringLiteral("U"),
3004                                QStringLiteral("u"),
3005                                QStringLiteral("F"),
3006                                QStringLiteral("f"),
3007                                QStringLiteral("C"),
3008                                QStringLiteral("c"),
3009                                QStringLiteral("Y"),
3010                                QStringLiteral("y"),
3011                                QStringLiteral("W"),
3012                                QStringLiteral("w"),
3013 
3014                                // extra symbols
3015                                QStringLiteral("V"),
3016                                QStringLiteral("J"),
3017                                QStringLiteral("j"),
3018                                QStringLiteral("v"),
3019                                QStringLiteral("i")};
3020 
3021     // Unicode friendy codes for greek letters and symbols
3022     static QStringList unicodeFriendlyCode{// letters
3023                                            QStringLiteral("&Alpha;"),
3024                                            QStringLiteral("&alpha;"),
3025                                            QStringLiteral("&Beta;"),
3026                                            QStringLiteral("&beta;"),
3027                                            QStringLiteral("&Gamma;"),
3028                                            QStringLiteral("&gamma;"),
3029                                            QStringLiteral("&Delta;"),
3030                                            QStringLiteral("&delta;"),
3031                                            QStringLiteral("&Epsilon;"),
3032                                            QStringLiteral("&epsilon;"),
3033                                            QStringLiteral("&Zeta;"),
3034                                            QStringLiteral("&zeta;"),
3035                                            QStringLiteral("&Eta;"),
3036                                            QStringLiteral("&eta;"),
3037                                            QStringLiteral("&Theta;"),
3038                                            QStringLiteral("&theta;"),
3039                                            QStringLiteral("&Iota;"),
3040                                            QStringLiteral("Iota;"),
3041                                            QStringLiteral("&Kappa;"),
3042                                            QStringLiteral("&kappa;"),
3043                                            QStringLiteral("&Lambda;"),
3044                                            QStringLiteral("&lambda;"),
3045                                            QStringLiteral("&Mu;"),
3046                                            QStringLiteral("&mu;"),
3047                                            QStringLiteral("&Nu;"),
3048                                            QStringLiteral("&nu;"),
3049                                            QStringLiteral("&Xi;"),
3050                                            QStringLiteral("&xi;"),
3051                                            QStringLiteral("&Omicron;"),
3052                                            QStringLiteral("&omicron;"),
3053                                            QStringLiteral("&Pi;"),
3054                                            QStringLiteral("&pi;"),
3055                                            QStringLiteral("&Rho;"),
3056                                            QStringLiteral("&rho;"),
3057                                            QStringLiteral("&Sigma;"),
3058                                            QStringLiteral("&sigma;"),
3059                                            QStringLiteral("&Tua;"),
3060                                            QStringLiteral("&tau;"),
3061                                            QStringLiteral("&Upsilon;"),
3062                                            QStringLiteral("&upsilon;"),
3063                                            QStringLiteral("&Phi;"),
3064                                            QStringLiteral("&phi;"),
3065                                            QStringLiteral("&Chi;"),
3066                                            QStringLiteral("&chi;"),
3067                                            QStringLiteral("&Psi;"),
3068                                            QStringLiteral("&psi;"),
3069                                            QStringLiteral("&Omega;"),
3070                                            QStringLiteral("&omega;"),
3071 
3072                                            // extra symbols
3073                                            QStringLiteral("&sigmaf;"),
3074                                            QStringLiteral("&thetasym;"),
3075                                            QStringLiteral("&#981;;") /* phi symbol, no friendly code */,
3076                                            QStringLiteral("&piv;"),
3077                                            QStringLiteral("&upsih;")};
3078 
3079     int index = symbols.indexOf(symbol);
3080     if (index != -1)
3081         return unicodeFriendlyCode.at(index);
3082     else
3083         return QString();
3084 }
3085 
3086 /*!
3087  * converts the string with Origin's syntax for text formatting/highlighting
3088  * to a string in the richtext/html format supported by Qt.
3089  * For the supported syntax, see:
3090  * https://www.originlab.com/doc/LabTalk/ref/Label-cmd
3091  * https://www.originlab.com/doc/Origin-Help/TextOb-Prop-Text-tab
3092  * https://doc.qt.io/qt-5/richtext-html-subset.html
3093  */
3094 QString OriginProjectParser::parseOriginTags(const QString& str) const {
3095     DEBUG(Q_FUNC_INFO << ", string = " << STDSTRING(str));
3096     QDEBUG("    UTF8 string: " << str.toUtf8());
3097     QString line = str;
3098 
3099     // replace %(...) tags
3100     //  QRegExp rxcol("\\%\\(\\d+\\)");
3101 
3102     // replace \l(x) (plot legend tags) with \\c{x}, where x is a digit
3103     line.replace(QRegularExpression(QStringLiteral("\\\\\\s*l\\s*\\(\\s*(\\d+)\\s*\\)")), QStringLiteral("\\c{\\1}"));
3104 
3105     // replace umlauts etc.
3106     line = replaceSpecialChars(line);
3107 
3108     // replace tabs (not really supported)
3109     line.replace(QLatin1Char('\t'), QLatin1String("&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;"));
3110 
3111     // In PCRE2 (which is what QRegularExpression uses) variable-length lookbehind is supposed to be
3112     // exprimental in Perl 5.30; which means it doesn't work at the moment, i.e. using a variable-length
3113     // negative lookbehind isn't valid syntax from QRegularExpression POV.
3114     // Ultimately we have to reverse the string and use a negative _lookahead_ instead.
3115     // The goal is to temporatily replace '(' and ')' that don't denote tags; this is so that we
3116     // can handle parenthesis that are inside the tag, e.g. '\b(bold (cf))', we want the '(cf)' part
3117     // to remain as is.
3118     const QRegularExpression nonTagsRe(QLatin1String(R"(\)([^)(]*)\((?!\s*([buigs\+\-]|\d{1,3}\s*[pc]|[\w ]+\s*:\s*f)\s*\\))"));
3119     QString linerev = strreverse(line);
3120     const QString lBracket = strreverse(QStringLiteral("&lbracket;"));
3121     const QString rBracket = strreverse(QStringLiteral("&rbracket;"));
3122     linerev.replace(nonTagsRe, rBracket + QStringLiteral("\\1") + lBracket);
3123 
3124     // change the line back to normal
3125     line = strreverse(linerev);
3126 
3127     // replace \-(...), \+(...), \b(...), \i(...), \u(...), \s(....), \g(...), \f:font(...),
3128     //  \c'number'(...), \p'size'(...) tags with equivalent supported HTML syntax
3129     const QRegularExpression tagsRe(QStringLiteral("\\\\\\s*([-+bgisu]|f:(\\w[\\w ]+)|[pc]\\s*(\\d+))\\s*\\(([^()]+?)\\)"));
3130     QRegularExpressionMatch rmatch;
3131     while (line.contains(tagsRe, &rmatch)) {
3132         QString rep;
3133         const QString tagText = rmatch.captured(4);
3134         const QString marker = rmatch.captured(1);
3135         if (marker.startsWith(QLatin1Char('-'))) {
3136             rep = QStringLiteral("<sub>%1</sub>").arg(tagText);
3137         } else if (marker.startsWith(QLatin1Char('+'))) {
3138             rep = QStringLiteral("<sup>%1</sup>").arg(tagText);
3139         } else if (marker.startsWith(QLatin1Char('b'))) {
3140             rep = QStringLiteral("<b>%1</b>").arg(tagText);
3141         } else if (marker.startsWith(QLatin1Char('g'))) { // greek symbols e.g. α φ
3142             rep = greekSymbol(tagText);
3143         } else if (marker.startsWith(QLatin1Char('i'))) {
3144             rep = QStringLiteral("<i>%1</i>").arg(tagText);
3145         } else if (marker.startsWith(QLatin1Char('s'))) {
3146             rep = QStringLiteral("<s>%1</s>").arg(tagText);
3147         } else if (marker.startsWith(QLatin1Char('u'))) {
3148             rep = QStringLiteral("<u>%1</u>").arg(tagText);
3149         } else if (marker.startsWith(QLatin1Char('f'))) {
3150             rep = QStringLiteral("<font face=\"%1\">%2</font>").arg(rmatch.captured(2).trimmed(), tagText);
3151         } else if (marker.startsWith(QLatin1Char('p'))) { // e.g. \p200(...), means use font-size 200%
3152             rep = QStringLiteral("<span style=\"font-size: %1%\">%2</span>").arg(rmatch.captured(3), tagText);
3153         } else if (marker.startsWith(QLatin1Char('c'))) {
3154             // e.g. \c12(...), set the text color to the corresponding color from
3155             // the color drop-down list in OriginLab
3156             const int colorIndex = rmatch.captured(3).toInt();
3157             Origin::Color c;
3158             c.type = Origin::Color::ColorType::Regular;
3159             c.regular = colorIndex <= 23 ? static_cast<Origin::Color::RegularColor>(colorIndex) : Origin::Color::RegularColor::Black;
3160             QColor color = OriginProjectParser::color(c);
3161             rep = QStringLiteral("<span style=\"color: %1\">%2</span>").arg(color.name(), tagText);
3162         }
3163         line.replace(rmatch.capturedStart(0), rmatch.capturedLength(0), rep);
3164     }
3165 
3166     // put non-tag '(' and ')' back in their places
3167     line.replace(QLatin1String("&lbracket;"), QLatin1String("("));
3168     line.replace(QLatin1String("&rbracket;"), QLatin1String(")"));
3169 
3170     // special characters
3171     line.replace(QRegularExpression(QStringLiteral("\\\\\\((\\d+)\\)")), QLatin1String("&#\\1;"));
3172 
3173     DEBUG(" result: " << STDSTRING(line));
3174 
3175     return line;
3176 }