Compare commits

...

16 Commits

Author SHA1 Message Date
Laurent Trinques ffbcd12d9b Revert "Update-UI-Chinese-translation" 2026-06-11 12:22:45 +02:00
Laurent Trinques 0a124f6695 Merge pull request #484 from zi-mozhuang/master
Update-UI-Chinese-translation
2026-06-11 12:19:03 +02:00
Laurent Trinques 3f6f99b50f Merge pull request #485 from ispyisail/fix-cli-pdf-grid
CLI export: don't draw the editor grid in PDF/PNG/SVG output
2026-06-11 12:15:42 +02:00
Shane Ringrose 42b64a7f0a CLI export: disable the editor grid in rendered output
renderDiagram() had a no-op stub: was_drawing_grid was set to false and
Q_UNUSED'd, so the editor grid still leaked into exported PDF/PNG/SVG.
Toggle Diagram::setDisplayGrid(false) around the render and restore the
previous state afterwards. Fixes all three export formats (they share
renderDiagram).

Reported by scorpio810 on #483.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 22:00:07 +12:00
Laurent Trinques 361719ca74 Fix my error in .pro 2026-06-11 11:39:25 +02:00
Laurent Trinques 181e2b555d Fix missing closing parenthesis in function call in .pro 2026-06-11 11:10:08 +02:00
Laurent Trinques 420512595d Update qelectrotech.pro
add sourc'es files missing in .pro Add headless command-line export (PDF / PNG / SVG / cable-list / wire-list)
#483
2026-06-11 11:07:25 +02:00
zi-mozhuang 9b4bed361d Update qet_zh.ts 2026-06-11 16:59:31 +08:00
Laurent Trinques 339bc8700b Merge pull request #483 from ispyisail/cli-export-master
Add headless command-line export (PDF / PNG / SVG / cable-list / wire-list)
2026-06-11 10:53:23 +02:00
Shane Ringrose 87d5ae5580 CLI export: add wiring list and wire-number list (CSV)
Extends the headless command-line export with two CSV outputs:

  qelectrotech --export-cables <project.qet> <output.csv>   wiring list
  qelectrotech --export-wires  <project.qet> <output.csv>   wire numbers

- --export-cables reuses WiringListExport (one row per conductor).
- --export-wires reuses ConductorNumExport::wiresNum() (distinct wire numbers).

WiringListExport::toCsv() mixed CSV generation with the file dialog and
writing.  Extracted the generation into a new const method toCsvString()
that returns the CSV; toCsv() now calls it and writes the result.  This
makes the wiring list usable headlessly with no behavioural change to the
GUI export.

Addresses part of the CLI export requests (#162, #309): @pkess specifically
asked to "export all connections as a list".
2026-06-11 12:39:43 +12:00
Shane Ringrose 1070179617 Add headless command-line export (PDF/PNG/SVG)
Implements the long-requested batch/headless export
(bugtracker #171, GitHub #309): render a project's diagrams to files
without opening the GUI.

  qelectrotech --export-pdf <project.qet> <output.pdf>   one multi-page PDF
  qelectrotech --export-png <project.qet> <output_dir>   one PNG per diagram
  qelectrotech --export-svg <project.qet> <output_dir>   one SVG per diagram

main.cpp detects an export request before SingleApplication is created (so the
arguments are not forwarded to a running instance), spins up a plain
QApplication for rendering, and exits with the export's status code.

Rendering reuses Diagram::render() over
BorderTitleBlock::borderAndTitleBlockRect(), the same geometry the GUI
print/export path uses, so output matches the editor. Image files are named
NN_Title.<ext>.

New files: sources/cli_export.{h,cpp}, registered in
cmake/qet_compilation_vars.cmake.
2026-06-11 11:10:09 +12:00
Laurent Trinques 6c4711a8d0 Update qet_compilation_vars.cmake 2026-06-07 15:11:06 +02:00
Laurent Trinques 8a8a338a2e Update CMakeLists.txt 2026-06-07 15:06:23 +02:00
Laurent Trinques 407cc7a4c2 Update CMakeLists.txt 2026-06-07 14:59:36 +02:00
Laurent Trinques 5cb8930732 Update fetch_pugixml.cmake 2026-06-07 14:57:29 +02:00
Laurent Trinques a24acfac24 Update fetch_pugixml.cmake 2026-06-07 14:57:05 +02:00
8 changed files with 371 additions and 23 deletions
+2 -1
View File
@@ -16,7 +16,7 @@
include(cmake/hoto_update_cmake_message.cmake)
cmake_minimum_required(VERSION 3.14...3.19 FATAL_ERROR)
cmake_minimum_required(VERSION 3.5...4.2)
project(qelectrotech
VERSION 0.100.1
@@ -145,6 +145,7 @@ target_include_directories(
${QET_DIR}/sources/dataBase/ui
${QET_DIR}/sources/factory/ui
${QET_DIR}/sources/print
${QET_DIR}/sources/svg
)
install(TARGETS ${PROJECT_NAME})
+1 -1
View File
@@ -25,7 +25,7 @@ if(BUILD_PUGIXML)
FetchContent_Declare(
pugixml
GIT_REPOSITORY https://github.com/zeux/pugixml.git
GIT_TAG v1.11.4)
GIT_TAG v1.15)
FetchContent_MakeAvailable(pugixml)
else()
+6
View File
@@ -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
@@ -500,6 +502,8 @@ set(QET_SRC_FILES
${QET_DIR}/sources/SearchAndReplace/ui/replacefoliowidget.h
${QET_DIR}/sources/SearchAndReplace/ui/searchandreplacewidget.cpp
${QET_DIR}/sources/SearchAndReplace/ui/searchandreplacewidget.h
${QET_DIR}/sources/svg/qetsvg.cpp
${QET_DIR}/sources/svg/qetsvg.h
${QET_DIR}/sources/titleblock/dimension.cpp
${QET_DIR}/sources/titleblock/dimension.h
@@ -714,6 +718,8 @@ set(QET_SRC_FILES
${QET_DIR}/sources/xml/terminalstripitemxml.cpp
${QET_DIR}/sources/xml/terminalstripitemxml.h
${QET_DIR}/sources/xml/terminalstriplayoutpatternxml.cpp
${QET_DIR}/sources/xml/terminalstriplayoutpatternxml.h
)
set(TS_FILES
+249
View File
@@ -0,0 +1,249 @@
/*
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);
// Export without the editor grid: drawBackground() only paints it when
// draw_grid_ is set (default true), so toggle it off around the render
// and restore it afterwards.
const bool was_drawing_grid = diagram->displayGrid();
diagram->setDisplayGrid(false);
diagram->render(&painter, target, source, Qt::KeepAspectRatio);
diagram->setDisplayGrid(was_drawing_grid);
}
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
+60
View File
@@ -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
+16
View File
@@ -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
+32 -21
View File
@@ -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;
}
+5
View File
@@ -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;