mirror of
https://github.com/qelectrotech/qelectrotech-source-mirror.git
synced 2026-06-14 00:43:14 +02:00
Merge pull request #483 from ispyisail/cli-export-master
Add headless command-line export (PDF / PNG / SVG / cable-list / wire-list)
This commit is contained in:
@@ -107,6 +107,8 @@ set(QET_RES_FILES
|
|||||||
${QET_DIR}/sources/ui/configpage/generalconfigurationpage.ui
|
${QET_DIR}/sources/ui/configpage/generalconfigurationpage.ui
|
||||||
)
|
)
|
||||||
set(QET_SRC_FILES
|
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.cpp
|
||||||
${QET_DIR}/sources/borderproperties.h
|
${QET_DIR}/sources/borderproperties.h
|
||||||
${QET_DIR}/sources/bordertitleblock.cpp
|
${QET_DIR}/sources/bordertitleblock.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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
#include "cli_export.h"
|
||||||
|
|
||||||
|
#include "bordertitleblock.h"
|
||||||
|
#include "conductornumexport.h"
|
||||||
|
#include "diagram.h"
|
||||||
|
#include "qetproject.h"
|
||||||
|
#include "wiringlistexport.h"
|
||||||
|
|
||||||
|
#include <QDir>
|
||||||
|
#include <QFile>
|
||||||
|
#include <QFileInfo>
|
||||||
|
#include <QPainter>
|
||||||
|
#include <QPdfWriter>
|
||||||
|
#include <QSvgGenerator>
|
||||||
|
#include <QTextStream>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
QTextStream out(stdout);
|
||||||
|
QTextStream err(stderr);
|
||||||
|
|
||||||
|
/// All export option flags, mapped to a short format name.
|
||||||
|
const QHash<QString, QString> &exportFlags()
|
||||||
|
{
|
||||||
|
static const QHash<QString, QString> 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<Diagram *> 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<Diagram *> 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
|
||||||
|
<< " <project.qet> <output>\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
|
||||||
@@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
#ifndef CLI_EXPORT_H
|
||||||
|
#define CLI_EXPORT_H
|
||||||
|
|
||||||
|
#include <QStringList>
|
||||||
|
|
||||||
|
/**
|
||||||
|
@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 <project.qet> <output.pdf>
|
||||||
|
qelectrotech --export-png <project.qet> <output_dir>
|
||||||
|
qelectrotech --export-svg <project.qet> <output_dir>
|
||||||
|
qelectrotech --export-cables <project.qet> <output.csv>
|
||||||
|
qelectrotech --export-wires <project.qet> <output.csv>
|
||||||
|
|
||||||
|
PDF: one multi-page document (one diagram per page).
|
||||||
|
PNG/SVG: one file per diagram, named <output_dir>/<NN>_<title>.<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
|
||||||
@@ -15,6 +15,7 @@
|
|||||||
You should have received a copy of the GNU General Public License
|
You should have received a copy of the GNU General Public License
|
||||||
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
#include "cli_export.h"
|
||||||
#include "machine_info.h"
|
#include "machine_info.h"
|
||||||
#include "qet.h"
|
#include "qet.h"
|
||||||
#include "qetapp.h"
|
#include "qetapp.h"
|
||||||
@@ -22,6 +23,8 @@
|
|||||||
#include "utils/macosxopenevent.h"
|
#include "utils/macosxopenevent.h"
|
||||||
#include "utils/qetsettings.h"
|
#include "utils/qetsettings.h"
|
||||||
|
|
||||||
|
#include <QApplication>
|
||||||
|
|
||||||
#include <QStyleFactory>
|
#include <QStyleFactory>
|
||||||
#include <QtConcurrentRun>
|
#include <QtConcurrentRun>
|
||||||
|
|
||||||
@@ -194,6 +197,19 @@ QGuiApplication::setHighDpiScaleFactorRoundingPolicy(QetSettings::hdpiScaleFacto
|
|||||||
#endif
|
#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);
|
SingleApplication app(argc, argv, true);
|
||||||
#ifdef Q_OS_MACOS
|
#ifdef Q_OS_MACOS
|
||||||
//Handle the opening of QET when user double click on a .qet .elmt .tbt file
|
//Handle the opening of QET when user double click on a .qet .elmt .tbt file
|
||||||
|
|||||||
@@ -151,13 +151,39 @@ void WiringListExport::toCsv()
|
|||||||
{
|
{
|
||||||
if (!m_project) return;
|
if (!m_project) return;
|
||||||
|
|
||||||
QDomDocument doc = m_project->toXml();
|
const QString csv = toCsvString();
|
||||||
|
if (csv.isEmpty()) {
|
||||||
if (doc.isNull()) {
|
|
||||||
QMessageBox::warning(m_parent, tr("Erreur"), tr("Impossible de lire la structure en mémoire du projet."));
|
QMessageBox::warning(m_parent, tr("Erreur"), tr("Impossible de lire la structure en mémoire du projet."));
|
||||||
return;
|
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;
|
QSet<QString> conductorDefinitionTypes;
|
||||||
QDomElement rootElem = doc.documentElement();
|
QDomElement rootElem = doc.documentElement();
|
||||||
QDomElement collection = rootElem.firstChildElement("collection");
|
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());
|
QMap<QString, ElementInfo> elementsInfo = collectElementsInfo(doc.documentElement());
|
||||||
QList<ConductorData> conductors = collectConductors(doc.documentElement());
|
QList<ConductorData> conductors = collectConductors(doc.documentElement());
|
||||||
|
|
||||||
@@ -353,7 +364,8 @@ void WiringListExport::toCsv()
|
|||||||
return a.terminalname2 < b.terminalname2;
|
return a.terminalname2 < b.terminalname2;
|
||||||
});
|
});
|
||||||
|
|
||||||
QTextStream out(&file);
|
QString csv;
|
||||||
|
QTextStream out(&csv);
|
||||||
out << tr("Page", "Wiring list CSV header") << ";"
|
out << tr("Page", "Wiring list CSV header") << ";"
|
||||||
<< tr("Composant 1", "Wiring list CSV header") << ";"
|
<< tr("Composant 1", "Wiring list CSV header") << ";"
|
||||||
<< tr("Borne 1", "Wiring list CSV header") << ";"
|
<< tr("Borne 1", "Wiring list CSV header") << ";"
|
||||||
@@ -376,6 +388,5 @@ void WiringListExport::toCsv()
|
|||||||
<< c.function << "\n";
|
<< c.function << "\n";
|
||||||
}
|
}
|
||||||
|
|
||||||
file.close();
|
return csv;
|
||||||
QMessageBox::information(m_parent, tr("Export réussi"), tr("Le plan de câblage a été exporté avec succès !"));
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,6 +45,11 @@ class WiringListExport : public QObject
|
|||||||
public:
|
public:
|
||||||
explicit WiringListExport(QETProject *project, QWidget *parent = nullptr);
|
explicit WiringListExport(QETProject *project, QWidget *parent = nullptr);
|
||||||
void toCsv();
|
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:
|
private:
|
||||||
QETProject *m_project;
|
QETProject *m_project;
|
||||||
|
|||||||
Reference in New Issue
Block a user