Add image saving to SVG format

This allows to create images independent from screen resolution
Replaced copy-pasted screenshot code with the single implementation
This commit is contained in:
alexey.lysiuk 2025-11-12 15:14:37 +02:00
parent bbd1bce0b8
commit 56dbec82d4
14 changed files with 96 additions and 62 deletions

View file

@ -17,7 +17,7 @@ jobs:
- name: Install dependencies - name: Install dependencies
run: | run: |
sudo apt-get update sudo apt-get update
sudo apt-get install -y libusb-1.0-0-dev qt6-tools-dev qt6-base-dev sudo apt-get install -y libusb-1.0-0-dev qt6-tools-dev qt6-base-dev libqt6svg6-dev libgl-dev
qtchooser -install qt6 $(which qmake6) qtchooser -install qt6 $(which qmake6)
- name: Get build timestamp - name: Get build timestamp

View file

@ -19,7 +19,7 @@ jobs:
- name: Install dependencies - name: Install dependencies
run: | run: |
sudo apt-get update sudo apt-get update
sudo apt-get install -y libusb-1.0-0-dev qt6-tools-dev qt6-base-dev sudo apt-get install -y libusb-1.0-0-dev qt6-tools-dev qt6-base-dev qt6-svg-dev
- name: Build application - name: Build application
run: | run: |

View file

@ -17,7 +17,7 @@ jobs:
- name: Install dependencies - name: Install dependencies
run: | run: |
sudo apt-get update sudo apt-get update
sudo apt-get install -y libusb-1.0-0-dev qt6-tools-dev qt6-base-dev zip sudo apt-get install -y libusb-1.0-0-dev qt6-tools-dev qt6-base-dev libqt6svg6-dev libgl-dev zip
qtchooser -install qt6 $(which qmake6) qtchooser -install qt6 $(which qmake6)
- name: Get app version - name: Get app version
@ -72,7 +72,7 @@ jobs:
- name: Install dependencies - name: Install dependencies
run: | run: |
sudo apt-get update sudo apt-get update
sudo apt-get install -y libusb-1.0-0-dev qt6-tools-dev qt6-base-dev sudo apt-get install -y libusb-1.0-0-dev qt6-tools-dev qt6-base-dev qt6-svg-dev
- name: Get app version - name: Get app version
id: id_version id: id_version

View file

@ -17,7 +17,7 @@ jobs:
- name: Install dependencies - name: Install dependencies
run: | run: |
sudo apt-get update sudo apt-get update
sudo apt-get install -y libusb-1.0-0-dev qt6-tools-dev qt6-base-dev sudo apt-get install -y libusb-1.0-0-dev qt6-tools-dev qt6-base-dev libqt6svg6-dev libgl-dev
qtchooser -install qt6 $(which qmake6) qtchooser -install qt6 $(which qmake6)
- name: Build Tests - name: Build Tests

View file

@ -164,6 +164,7 @@ HEADERS += \
preferences.h \ preferences.h \
savable.h \ savable.h \
scpi.h \ scpi.h \
screenshot.h \
streamingserver.h \ streamingserver.h \
tcpserver.h \ tcpserver.h \
touchstone.h \ touchstone.h \
@ -320,6 +321,7 @@ SOURCES += \
preferences.cpp \ preferences.cpp \
savable.cpp \ savable.cpp \
scpi.cpp \ scpi.cpp \
screenshot.cpp \
streamingserver.cpp \ streamingserver.cpp \
tcpserver.cpp \ tcpserver.cpp \
touchstone.cpp \ touchstone.cpp \
@ -338,7 +340,7 @@ mac{
PKGCONFIG += libusb-1.0 PKGCONFIG += libusb-1.0
} }
QT += widgets network QT += widgets network svg
FORMS += \ FORMS += \
Calibration/CalStandardLineEditDialog.ui \ Calibration/CalStandardLineEditDialog.ui \

View file

@ -7,6 +7,7 @@
#include "fftcomplex.h" #include "fftcomplex.h"
#include "preferences.h" #include "preferences.h"
#include "appwindow.h" #include "appwindow.h"
#include "screenshot.h"
#include <random> #include <random>
#include <thread> #include <thread>
@ -403,17 +404,7 @@ void EyeDiagramPlot::updateContextMenu()
auto image = new QAction("Save image...", contextmenu); auto image = new QAction("Save image...", contextmenu);
contextmenu->addAction(image); contextmenu->addAction(image);
connect(image, &QAction::triggered, this, [=]() { connect(image, &QAction::triggered, this, [=]() {
auto filename = QFileDialog::getSaveFileName(nullptr, "Save plot image", Preferences::getInstance().UISettings.Paths.image, "PNG image files (*.png)", nullptr, Preferences::QFileDialogOptions()); SaveScreenshot(this);
if(filename.isEmpty()) {
// aborted selection
return;
}
Preferences::getInstance().UISettings.Paths.image = QFileInfo(filename).path();
if(filename.endsWith(".png")) {
filename.chop(4);
}
filename += ".png";
grab().save(filename);
}); });
contextmenu->addSection("Traces"); contextmenu->addSection("Traces");

View file

@ -3,6 +3,7 @@
#include "Marker/marker.h" #include "Marker/marker.h"
#include "Util/util.h" #include "Util/util.h"
#include "preferences.h" #include "preferences.h"
#include "screenshot.h"
#include <QFileDialog> #include <QFileDialog>
@ -253,17 +254,7 @@ void TracePolar::updateContextMenu()
auto image = new QAction("Save image...", contextmenu); auto image = new QAction("Save image...", contextmenu);
contextmenu->addAction(image); contextmenu->addAction(image);
connect(image, &QAction::triggered, [=]() { connect(image, &QAction::triggered, [=]() {
auto filename = QFileDialog::getSaveFileName(nullptr, "Save plot image", Preferences::getInstance().UISettings.Paths.image, "PNG image files (*.png)", nullptr, Preferences::QFileDialogOptions()); SaveScreenshot(this);
if(filename.isEmpty()) {
// aborted selection
return;
}
Preferences::getInstance().UISettings.Paths.image = QFileInfo(filename).path();
if(filename.endsWith(".png")) {
filename.chop(4);
}
filename += ".png";
grab().save(filename);
}); });
auto createMarker = contextmenu->addAction("Add marker here"); auto createMarker = contextmenu->addAction("Add marker here");

View file

@ -6,6 +6,7 @@
#include "waterfallaxisdialog.h" #include "waterfallaxisdialog.h"
#include "appwindow.h" #include "appwindow.h"
#include "tracexyplot.h" #include "tracexyplot.h"
#include "screenshot.h"
#include <QFileDialog> #include <QFileDialog>
#include <QPainter> #include <QPainter>
@ -213,17 +214,7 @@ void TraceWaterfall::updateContextMenu()
auto image = new QAction("Save image...", contextmenu); auto image = new QAction("Save image...", contextmenu);
contextmenu->addAction(image); contextmenu->addAction(image);
connect(image, &QAction::triggered, [=]() { connect(image, &QAction::triggered, [=]() {
auto filename = QFileDialog::getSaveFileName(nullptr, "Save plot image", Preferences::getInstance().UISettings.Paths.image, "PNG image files (*.png)", nullptr, Preferences::QFileDialogOptions()); SaveScreenshot(this);
if(filename.isEmpty()) {
// aborted selection
return;
}
Preferences::getInstance().UISettings.Paths.image = QFileInfo(filename).path();
if(filename.endsWith(".png")) {
filename.chop(4);
}
filename += ".png";
grab().save(filename);
}); });
contextmenu->addSection("Traces"); contextmenu->addSection("Traces");

View file

@ -9,6 +9,7 @@
#include "preferences.h" #include "preferences.h"
#include "appwindow.h" #include "appwindow.h"
#include "ui_XYPlotConstantLineEditDialog.h" #include "ui_XYPlotConstantLineEditDialog.h"
#include "screenshot.h"
#include <QGridLayout> #include <QGridLayout>
#include <cmath> #include <cmath>
@ -349,17 +350,7 @@ void TraceXYPlot::updateContextMenu()
auto image = new QAction("Save image...", contextmenu); auto image = new QAction("Save image...", contextmenu);
contextmenu->addAction(image); contextmenu->addAction(image);
connect(image, &QAction::triggered, [=]() { connect(image, &QAction::triggered, [=]() {
auto filename = QFileDialog::getSaveFileName(nullptr, "Save plot image", Preferences::getInstance().UISettings.Paths.image, "PNG image files (*.png)", nullptr, Preferences::QFileDialogOptions()); SaveScreenshot(this);
if(filename.isEmpty()) {
// aborted selection
return;
}
Preferences::getInstance().UISettings.Paths.image = QFileInfo(filename).path();
if(filename.endsWith(".png")) {
filename.chop(4);
}
filename += ".png";
grab().save(filename);
}); });
auto createMarker = contextmenu->addAction("Add marker here"); auto createMarker = contextmenu->addAction("Add marker here");

View file

@ -6,6 +6,7 @@
#include "CustomWidgets/informationbox.h" #include "CustomWidgets/informationbox.h"
#include "ui_main.h" #include "ui_main.h"
#include "screenshot.h"
#include <QPushButton> #include <QPushButton>
#include <QSettings> #include <QSettings>
@ -141,17 +142,7 @@ Mode::Type Mode::TypeFromName(QString s)
void Mode::saveSreenshot() void Mode::saveSreenshot()
{ {
auto filename = QFileDialog::getSaveFileName(nullptr, "Save plot image", Preferences::getInstance().UISettings.Paths.image, "PNG image files (*.png)", nullptr, Preferences::QFileDialogOptions()); SaveScreenshot(central);
if(filename.isEmpty()) {
// aborted selection
return;
}
Preferences::getInstance().UISettings.Paths.image = QFileInfo(filename).path();
if(filename.endsWith(".png")) {
filename.chop(4);
}
filename += ".png";
central->grab().save(filename);
} }
void Mode::finalize(QWidget *centralWidget) void Mode::finalize(QWidget *centralWidget)

View file

@ -229,6 +229,7 @@ public:
QString pref; QString pref;
QString firmware; QString firmware;
} Paths; } Paths;
qsizetype saveImageFilterIndex;
} UISettings; } UISettings;
bool TCPoverride; // in case of manual port specification via command line bool TCPoverride; // in case of manual port specification via command line
@ -413,6 +414,7 @@ private:
{&UISettings.Paths.limitLines, "UISettings.Paths.limitLines", ""}, {&UISettings.Paths.limitLines, "UISettings.Paths.limitLines", ""},
{&UISettings.Paths.pref, "UISettings.Paths.pref", ""}, {&UISettings.Paths.pref, "UISettings.Paths.pref", ""},
{&UISettings.Paths.firmware, "UISettings.Paths.firmware", ""}, {&UISettings.Paths.firmware, "UISettings.Paths.firmware", ""},
{&UISettings.saveImageFilterIndex, "UISettings.saveImageFilterIndex", 0},
}}; }};
}; };

View file

@ -0,0 +1,65 @@
#include "screenshot.h"
#include "preferences.h"
#include <QCoreApplication>
#include <QPainter>
#include <QSvgGenerator>
void SaveScreenshot(QWidget *widget)
{
Q_ASSERT(widget != nullptr);
const QStringList extensions = QStringList() << "png" << "svg";
QStringList filters;
for (const QString& ext: extensions) {
filters << QString("%1 image files (*.%2)").arg(ext.toUpper(), ext);
}
auto& settings = Preferences::getInstance().UISettings;
const QString filterString = filters.join(";;");
qsizetype filterIndex = qBound(0, settings.saveImageFilterIndex, filters.size() - 1);
QString selectedFilter = filters[filterIndex];
auto filename = QFileDialog::getSaveFileName(nullptr, "Save plot image", settings.Paths.image, filterString, &selectedFilter, Preferences::QFileDialogOptions());
if(filename.isEmpty()) {
// aborted selection
return;
}
filterIndex = filters.indexOf(selectedFilter);
const QString& extension = extensions[filterIndex];
if(!filename.endsWith(extension)) {
filename += '.' + extension;
}
settings.Paths.image = QFileInfo(filename).path();
settings.saveImageFilterIndex = filterIndex;
switch (filterIndex)
{
case 0: // PNG
widget->grab().save(filename);
break;
case 1: // SVG
{
QSvgGenerator generator;
generator.setFileName(filename);
generator.setViewBox(QRect(QPoint(), widget->size()));
generator.setTitle(QCoreApplication::applicationName());
generator.setDescription(QString("Created by %1 %2").arg(QCoreApplication::applicationName(), QCoreApplication::applicationVersion()));
QPainter painter;
painter.begin(&generator);
widget->render(&painter);
painter.end();
break;
}
default:
Q_ASSERT(false);
break;
}
}

View file

@ -0,0 +1,8 @@
#ifndef SCREENSHOT_H
#define SCREENSHOT_H
class QWidget;
void SaveScreenshot(QWidget *widget);
#endif // SCREENSHOT_H

View file

@ -1,4 +1,4 @@
QT += testlib widgets network QT += testlib widgets network svg
CONFIG += qt console warn_on depend_includepath testcase CONFIG += qt console warn_on depend_includepath testcase
CONFIG -= app_bundle CONFIG -= app_bundle
@ -155,6 +155,7 @@ SOURCES += \
../LibreVNA-GUI/preferences.cpp \ ../LibreVNA-GUI/preferences.cpp \
../LibreVNA-GUI/savable.cpp \ ../LibreVNA-GUI/savable.cpp \
../LibreVNA-GUI/scpi.cpp \ ../LibreVNA-GUI/scpi.cpp \
../LibreVNA-GUI/screenshot.cpp \
../LibreVNA-GUI/tcpserver.cpp \ ../LibreVNA-GUI/tcpserver.cpp \
../LibreVNA-GUI/streamingserver.cpp \ ../LibreVNA-GUI/streamingserver.cpp \
../LibreVNA-GUI/touchstone.cpp \ ../LibreVNA-GUI/touchstone.cpp \
@ -359,6 +360,7 @@ HEADERS += \
../LibreVNA-GUI/preferences.h \ ../LibreVNA-GUI/preferences.h \
../LibreVNA-GUI/savable.h \ ../LibreVNA-GUI/savable.h \
../LibreVNA-GUI/scpi.h \ ../LibreVNA-GUI/scpi.h \
../LibreVNA-GUI/screenshot.h \
../LibreVNA-GUI/tcpserver.h \ ../LibreVNA-GUI/tcpserver.h \
../LibreVNA-GUI/streamingserver.h \ ../LibreVNA-GUI/streamingserver.h \
../LibreVNA-GUI/touchstone.h \ ../LibreVNA-GUI/touchstone.h \