Add export mapping scheme to image feature

- Реализован метод export_to_image в canvas.py для рендеринга всей сцены маппинга (с учетом кастомного фона, сетки и точек) в файл изображения с полями и масштабированием.
- Добавлено действие export_image (Экспортировать в изображение...) в меню Файл с хоткеем Ctrl+Shift+E и вызовом диалога сохранения файла.
- Добавлен интеграционный тест test_export_mapping_to_image в qa_smoke.py для проверки экспорта схемы в PNG.
This commit is contained in:
Данил Ярош 2026-07-31 13:04:39 +03:00
parent 91189d6d37
commit 31b9028fc2
3 changed files with 101 additions and 1 deletions

View File

@ -4,7 +4,7 @@ import math
from pathlib import Path
from PySide6.QtCore import QPointF, QRectF, QSize, Qt, Signal
from PySide6.QtGui import QColor, QBrush, QImageReader, QPainter, QPainterPath, QPen, QPixmap, QTransform
from PySide6.QtGui import QColor, QBrush, QImage, QImageReader, QPainter, QPainterPath, QPen, QPixmap, QTransform
from PySide6.QtWidgets import (
QGraphicsEllipseItem,
QGraphicsItem,
@ -1489,3 +1489,40 @@ class LedCanvas(QGraphicsView):
@staticmethod
def _move_scrollbar(scrollbar: QScrollBar, delta: int) -> None:
scrollbar.setValue(scrollbar.value() - delta)
def export_to_image(self, file_path: str) -> bool:
scene = self.scene()
if scene is None:
return False
rect = scene.itemsBoundingRect()
if rect.isEmpty():
return False
padding = max(rect.width(), rect.height()) * 0.05
rect.adjust(-padding, -padding, padding, padding)
target_width = 2048
aspect_ratio = rect.height() / rect.width()
target_height = int(target_width * aspect_ratio)
if target_height > 4096:
target_height = 4096
target_width = int(target_height / aspect_ratio)
image = QImage(target_width, target_height, QImage.Format.Format_ARGB32)
bg_brush = scene.backgroundBrush()
if bg_brush.style() != Qt.BrushStyle.NoBrush:
image.fill(bg_brush.color())
else:
image.fill(QColor("#2b2b2b" if self._theme == "dark" else "#ffffff"))
painter = QPainter(image)
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
painter.setRenderHint(QPainter.RenderHint.TextAntialiasing)
scene.render(painter, QRectF(0, 0, target_width, target_height), rect)
painter.end()
return image.save(file_path)

View File

@ -337,6 +337,14 @@ class MainWindow(QMainWindow):
add_action("open", "Открыть", self.open_file, "folder-open", QKeySequence.Open)
add_action("save", "Сохранить", self.save_file, "save", QKeySequence.Save)
add_action("save_as", "Сохранить как", self.save_file_as, "save-all", QKeySequence.SaveAs)
add_action(
"export_image",
"Экспортировать в изображение...",
self.export_mapping_image,
"camera",
QKeySequence("Ctrl+Shift+E"),
"Экспортировать текущую схему маппинга в изображение (PNG, JPEG)"
)
add_action("settings", "Параметры", self.open_settings, "settings", QKeySequence("Ctrl+,"))
add_action(
"generate_script",
@ -439,6 +447,7 @@ class MainWindow(QMainWindow):
file_menu.addAction(self.actions["open"])
file_menu.addAction(self.actions["save"])
file_menu.addAction(self.actions["save_as"])
file_menu.addAction(self.actions["export_image"])
file_menu.addSeparator()
file_menu.addAction(self.actions["generate_script"])
file_menu.addSeparator()
@ -939,6 +948,38 @@ class MainWindow(QMainWindow):
if path:
self._save_to(Path(path))
def export_mapping_image(self) -> None:
if not self.document.points:
self.status.setText("Схема пуста. Экспорт отменён.")
self._refresh_ui_state()
return
default_name = "mapping.png"
if self.document.path is not None:
default_name = self.document.path.stem + ".png"
self._disable_shortcuts()
try:
file_path, _ = QFileDialog.getSaveFileName(
self,
"Экспортировать маппинг в изображение",
default_name,
"PNG Image (*.png);;JPEG Image (*.jpg *.jpeg)"
)
finally:
self._enable_shortcuts()
if not file_path:
return
success = self.canvas.export_to_image(file_path)
if success:
self.status.setText(f"Схема успешно экспортирована в: {file_path}")
else:
show_warning(self, "Ошибка экспорта", "Не удалось экспортировать схему в изображение.")
self.status.setText("Ошибка экспорта изображения.")
self._refresh_ui_state()
def generate_script(self, output_dir: str | Path | None = None) -> None:
if isinstance(output_dir, bool):
output_dir = None
@ -2137,6 +2178,7 @@ class MainWindow(QMainWindow):
state = {
"save": has_points and self._dirty,
"save_as": has_points,
"export_image": has_points,
"generate_script": has_points,
"undo": bool(self._undo_stack),
"redo": bool(self._redo_stack),

View File

@ -121,6 +121,7 @@ def main() -> None:
test_panel_mapping_generation_with_strategies,
test_block_renaming_and_naming_strategy,
test_line_renaming_and_tree_shortcuts,
test_export_mapping_to_image,
test_save_preserves_unknown_source_fields,
test_ordered_json_preserves_document_and_remaps_uids,
test_background_load_save_roundtrip,
@ -1132,6 +1133,26 @@ def test_line_renaming_and_tree_shortcuts() -> None:
window.close()
def test_export_mapping_to_image() -> None:
if MainWindow is None or QApplication is None:
return
app = QApplication.instance() or QApplication([])
window = MainWindow()
window.document.add_manual_point(100.0, 100.0)
window.canvas.set_document(window.document)
window._refresh_ui_state()
with tempfile.TemporaryDirectory() as temp_dir:
image_path = Path(temp_dir) / "output.png"
success = window.canvas.export_to_image(str(image_path))
assert success
assert image_path.exists()
assert image_path.stat().st_size > 0
window.close()
def test_save_preserves_unknown_source_fields() -> None:
with tempfile.TemporaryDirectory() as temp_dir:
source = Path(temp_dir) / "custom.json"