diff --git a/cmake/qet_compilation_vars.cmake b/cmake/qet_compilation_vars.cmake index 8224d09b2..cc759d437 100644 --- a/cmake/qet_compilation_vars.cmake +++ b/cmake/qet_compilation_vars.cmake @@ -107,6 +107,8 @@ set(QET_RES_FILES ${QET_DIR}/sources/ui/configpage/generalconfigurationpage.ui ) set(QET_SRC_FILES + ${QET_DIR}/sources/cli_export.cpp + ${QET_DIR}/sources/cli_export.h ${QET_DIR}/sources/borderproperties.cpp ${QET_DIR}/sources/borderproperties.h ${QET_DIR}/sources/bordertitleblock.cpp diff --git a/sources/cli_export.cpp b/sources/cli_export.cpp new file mode 100644 index 000000000..c3985375b --- /dev/null +++ b/sources/cli_export.cpp @@ -0,0 +1,245 @@ +/* + Copyright 2006-2025 The QElectroTech Team + This file is part of QElectroTech. + + QElectroTech is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + QElectroTech is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with QElectroTech. If not, see . +*/ +#include "cli_export.h" + +#include "bordertitleblock.h" +#include "conductornumexport.h" +#include "diagram.h" +#include "qetproject.h" +#include "wiringlistexport.h" + +#include +#include +#include +#include +#include +#include +#include + +namespace { + +QTextStream out(stdout); +QTextStream err(stderr); + +/// All export option flags, mapped to a short format name. +const QHash &exportFlags() +{ + static const QHash flags { + {"--export-pdf", "pdf"}, + {"--export-png", "png"}, + {"--export-svg", "svg"}, + {"--export-cables", "cables"}, + {"--export-wires", "wires"}, + }; + return flags; +} + +/// Pixel rect of a diagram's border + title block (the printable page area). +QRect diagramRect(Diagram *diagram) +{ + QRectF r = diagram->border_and_titleblock.borderAndTitleBlockRect(); + r.adjust(0, 0, 1, 1); // include the 1px border line + return r.toAlignedRect(); +} + +/// A filesystem-safe per-diagram file stem: "01_Title". +QString diagramStem(Diagram *diagram, int index) +{ + QString title = diagram->title(); + title.replace(QRegularExpression("[^\\w \\-]"), "_"); + title = title.simplified(); + if (title.isEmpty()) + title = "diagram"; + return QStringLiteral("%1_%2") + .arg(index, 2, 10, QChar('0')) + .arg(title); +} + +/// Render @p diagram into @p painter, fitting @p target to the page rect. +void renderDiagram(Diagram *diagram, QPainter &painter, const QRectF &target) +{ + const QRect source = diagramRect(diagram); + const bool was_drawing_grid = false; // export without the editor grid + Q_UNUSED(was_drawing_grid) + diagram->render(&painter, target, source, Qt::KeepAspectRatio); +} + +int exportPdf(QETProject &project, const QString &output) +{ + const QList diagrams = project.diagrams(); + if (diagrams.isEmpty()) { + err << "No diagrams to export.\n"; + return 1; + } + + QPdfWriter writer(output); + writer.setCreator("QElectroTech"); + writer.setResolution(96); + + QPainter painter; + bool first = true; + for (Diagram *diagram : diagrams) { + const QRect r = diagramRect(diagram); + // Match the page to the diagram (in points: 1px @ 96dpi = 0.75pt). + const QPageSize page(QSizeF(r.width() * 72.0 / 96.0, + r.height() * 72.0 / 96.0), + QPageSize::Point); + writer.setPageSize(page); + writer.setPageMargins(QMarginsF(0, 0, 0, 0)); + + if (first) { + if (!painter.begin(&writer)) { + err << "Cannot open '" << output << "' for writing.\n"; + return 1; + } + first = false; + } else { + writer.newPage(); + } + const QRectF target(0, 0, + writer.width(), writer.height()); + renderDiagram(diagram, painter, target); + } + painter.end(); + out << "Exported " << diagrams.size() << " page(s) -> " << output << "\n"; + return 0; +} + +int exportImages(QETProject &project, const QString &format, + const QString &out_dir) +{ + const QList diagrams = project.diagrams(); + if (diagrams.isEmpty()) { + err << "No diagrams to export.\n"; + return 1; + } + QDir().mkpath(out_dir); + + int index = 0; + for (Diagram *diagram : diagrams) { + ++index; + const QRect r = diagramRect(diagram); + const QString path = QDir(out_dir).filePath( + diagramStem(diagram, index) + "." + format); + + if (format == "svg") { + QSvgGenerator gen; + gen.setFileName(path); + gen.setSize(r.size()); + gen.setViewBox(QRect(0, 0, r.width(), r.height())); + gen.setTitle(diagram->title()); + QPainter painter(&gen); + renderDiagram(diagram, painter, QRectF(QPointF(0, 0), r.size())); + painter.end(); + } else { // png + QImage image(r.size(), QImage::Format_ARGB32); + image.fill(Qt::white); + QPainter painter(&image); + painter.setRenderHint(QPainter::Antialiasing, true); + renderDiagram(diagram, painter, QRectF(QPointF(0, 0), r.size())); + painter.end(); + if (!image.save(path)) { + err << "Failed to write '" << path << "'.\n"; + return 1; + } + } + out << " " << path << "\n"; + } + out << "Exported " << diagrams.size() << " diagram(s) -> " << out_dir << "\n"; + return 0; +} + +int exportCsv(QETProject &project, const QString &format, const QString &output) +{ + QString csv; + if (format == "cables") { + WiringListExport wle(&project, nullptr); + csv = wle.toCsvString(); + } else { // wires + ConductorNumExport cne(&project, nullptr); + csv = cne.wiresNum(); + } + if (csv.isEmpty()) { + err << "Nothing to export (empty list).\n"; + return 1; + } + + QFile file(output); + if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { + err << "Cannot open '" << output << "' for writing.\n"; + return 1; + } + QTextStream fout(&file); + fout << csv; + file.close(); + out << "Exported " << format << " list -> " << output << "\n"; + return 0; +} + +} // anonymous namespace + +namespace CLIExport { + +bool isExportRequest(const QStringList &args) +{ + for (const QString &a : args) + if (exportFlags().contains(a)) + return true; + return false; +} + +int run(const QStringList &args) +{ + QString flag, input, output; + for (int i = 0; i < args.size(); ++i) { + if (exportFlags().contains(args.at(i))) { + flag = args.at(i); + if (i + 2 < args.size()) { + input = args.at(i + 1); + output = args.at(i + 2); + } + break; + } + } + + if (input.isEmpty() || output.isEmpty()) { + err << "Usage: qelectrotech " << flag + << " \n"; + return 2; + } + if (!QFileInfo::exists(input)) { + err << "Project not found: " << input << "\n"; + return 2; + } + + QETProject project(input); + if (project.state() != QETProject::Ok) { + err << "Failed to open project: " << input + << " (state " << project.state() << ")\n"; + return 1; + } + + const QString format = exportFlags().value(flag); + if (format == "pdf") + return exportPdf(project, output); + if (format == "cables" || format == "wires") + return exportCsv(project, format, output); + return exportImages(project, format, output); +} + +} // namespace CLIExport diff --git a/sources/cli_export.h b/sources/cli_export.h new file mode 100644 index 000000000..9ac699307 --- /dev/null +++ b/sources/cli_export.h @@ -0,0 +1,60 @@ +/* + Copyright 2006-2025 The QElectroTech Team + This file is part of QElectroTech. + + QElectroTech is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + QElectroTech is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with QElectroTech. If not, see . +*/ +#ifndef CLI_EXPORT_H +#define CLI_EXPORT_H + +#include + +/** + @brief Headless command-line export. + + Implements the long-requested batch/headless export + (qelectrotech.org bugtracker #171, GitHub #309): render a project's + diagrams to files without opening the GUI. + + Detected and handled in main() before the GUI is created. +*/ +namespace CLIExport { + + /** + @brief True if @p args request a CLI export + (i.e. contain one of the export options). + */ + bool isExportRequest(const QStringList &args); + + /** + @brief Run the CLI export described by @p args. + @return process exit code (0 on success). + + Usage: + qelectrotech --export-pdf + qelectrotech --export-png + qelectrotech --export-svg + qelectrotech --export-cables + qelectrotech --export-wires + + PDF: one multi-page document (one diagram per page). + PNG/SVG: one file per diagram, named /_.<ext>. + cables: wiring list (one row per conductor) as CSV. + wires: list of distinct wire numbers as CSV. + */ + int run(const QStringList &args); + +} + +#endif // CLI_EXPORT_H diff --git a/sources/main.cpp b/sources/main.cpp index 43fa9c987..89e0eaf1c 100644 --- a/sources/main.cpp +++ b/sources/main.cpp @@ -15,6 +15,7 @@ You should have received a copy of the GNU General Public License along with QElectroTech. If not, see <http://www.gnu.org/licenses/>. */ +#include "cli_export.h" #include "machine_info.h" #include "qet.h" #include "qetapp.h" @@ -22,6 +23,8 @@ #include "utils/macosxopenevent.h" #include "utils/qetsettings.h" +#include <QApplication> + #include <QStyleFactory> #include <QtConcurrentRun> @@ -194,6 +197,19 @@ QGuiApplication::setHighDpiScaleFactorRoundingPolicy(QetSettings::hdpiScaleFacto #endif + // Headless command-line export: render a project to PDF/PNG/SVG without + // opening the GUI, then exit. Must be handled before SingleApplication + // (which would forward the args to an already-running instance). + { + QStringList raw_args; + for (int i = 0; i < argc; ++i) + raw_args << QString::fromLocal8Bit(argv[i]); + if (CLIExport::isExportRequest(raw_args)) { + QApplication export_app(argc, argv); + return CLIExport::run(export_app.arguments()); + } + } + SingleApplication app(argc, argv, true); #ifdef Q_OS_MACOS //Handle the opening of QET when user double click on a .qet .elmt .tbt file diff --git a/sources/wiringlistexport.cpp b/sources/wiringlistexport.cpp index 77b8d0d74..e688a1460 100644 --- a/sources/wiringlistexport.cpp +++ b/sources/wiringlistexport.cpp @@ -151,13 +151,39 @@ void WiringListExport::toCsv() { if (!m_project) return; - QDomDocument doc = m_project->toXml(); - - if (doc.isNull()) { + const QString csv = toCsvString(); + if (csv.isEmpty()) { QMessageBox::warning(m_parent, tr("Erreur"), tr("Impossible de lire la structure en mémoire du projet.")); return; } + QFileDialog dialog(m_parent); + dialog.setAcceptMode(QFileDialog::AcceptSave); + dialog.setWindowTitle(tr("Exporter le plan de câblage")); + dialog.setDefaultSuffix("csv"); + dialog.setNameFilter(tr("Fichiers CSV (*.csv)")); + + if (dialog.exec() != QDialog::Accepted) return; + QString fileName = dialog.selectedFiles().first(); + + QFile file(fileName); + if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { + QMessageBox::warning(m_parent, tr("Erreur"), tr("Impossible d'ouvrir le fichier pour l'écriture.")); + return; + } + QTextStream out(&file); + out << csv; + file.close(); + QMessageBox::information(m_parent, tr("Export réussi"), tr("Le plan de câblage a été exporté avec succès !")); +} + +QString WiringListExport::toCsvString() const +{ + if (!m_project) return QString(); + + QDomDocument doc = m_project->toXml(); + if (doc.isNull()) return QString(); + QSet<QString> conductorDefinitionTypes; QDomElement rootElem = doc.documentElement(); QDomElement collection = rootElem.firstChildElement("collection"); @@ -197,21 +223,6 @@ void WiringListExport::toCsv() } } - QFileDialog dialog(m_parent); - dialog.setAcceptMode(QFileDialog::AcceptSave); - dialog.setWindowTitle(tr("Exporter le plan de câblage")); - dialog.setDefaultSuffix("csv"); - dialog.setNameFilter(tr("Fichiers CSV (*.csv)")); - - if (dialog.exec() != QDialog::Accepted) return; - QString fileName = dialog.selectedFiles().first(); - - QFile file(fileName); - if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { - QMessageBox::warning(m_parent, tr("Erreur"), tr("Impossible d'ouvrir le fichier pour l'écriture.")); - return; - } - QMap<QString, ElementInfo> elementsInfo = collectElementsInfo(doc.documentElement()); QList<ConductorData> conductors = collectConductors(doc.documentElement()); @@ -353,7 +364,8 @@ void WiringListExport::toCsv() return a.terminalname2 < b.terminalname2; }); - QTextStream out(&file); + QString csv; + QTextStream out(&csv); out << tr("Page", "Wiring list CSV header") << ";" << tr("Composant 1", "Wiring list CSV header") << ";" << tr("Borne 1", "Wiring list CSV header") << ";" @@ -376,6 +388,5 @@ void WiringListExport::toCsv() << c.function << "\n"; } - file.close(); - QMessageBox::information(m_parent, tr("Export réussi"), tr("Le plan de câblage a été exporté avec succès !")); + return csv; } diff --git a/sources/wiringlistexport.h b/sources/wiringlistexport.h index b2766f394..b61779267 100644 --- a/sources/wiringlistexport.h +++ b/sources/wiringlistexport.h @@ -45,6 +45,11 @@ class WiringListExport : public QObject public: explicit WiringListExport(QETProject *project, QWidget *parent = nullptr); void toCsv(); + /** + Build the wiring-list CSV and return it as a string (no GUI). + Used by toCsv() and by the headless command-line export. + */ + QString toCsvString() const; private: QETProject *m_project;