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
+ << "