Embed background images in mapping files
- встраивать исходное изображение подложки в JSON вместе с метаданными и контрольной суммой - сохранять геометрию и параметры встроенной подложки при повторном редактировании - показывать значение «Встроенная» в диалоге подложки до выбора замены - загружать встроенное изображение непосредственно из JSON на холст - защищать существующий файл схемы при ошибке чтения подложки - расширить автоматические проверки до 161 сценария
This commit is contained in:
parent
f2108e053f
commit
0d4552ef35
@ -126,6 +126,8 @@ python -m venv .venv
|
||||
|
||||
JSON схемы хранит `coordinate_unit` и `grid_step_mm`. При первом открытии старого файла без единицы координат приложение определяет её один раз и затем сохраняет явно. Сохранение выполняется через временный файл и атомарную замену, поэтому исходный JSON не повреждается при ошибке записи.
|
||||
|
||||
Если у схемы есть подложка, при сохранении её исходное PNG- или JPEG-изображение встраивается в секцию `background.embedded_image` внутри JSON вместе с именем, MIME-типом и контрольной суммой. После этого схема не зависит от внешнего файла, а размеры и параметры подложки сохраняются при редактировании и переносе JSON. Для уже встроенного и не заменённого изображения диалог `Подложка` показывает в поле пути значение `Встроенная`. Чтобы заменить его, нажмите `Выбрать` и укажите новый файл; новое изображение будет встроено при следующем сохранении схемы.
|
||||
|
||||
В `vm_map.h` используются физические адреса из GUI:
|
||||
|
||||
- `strip = line_id - 1`
|
||||
|
||||
@ -24,13 +24,21 @@ except ImportError:
|
||||
|
||||
|
||||
class BackgroundDialog(QDialog):
|
||||
EMBEDDED_PATH_LABEL = "Встроенная"
|
||||
|
||||
def __init__(self, background: BackgroundImage, parent=None) -> None: # noqa: ANN001
|
||||
super().__init__(parent)
|
||||
self.setWindowTitle("Подложка")
|
||||
self.setMinimumWidth(460)
|
||||
|
||||
self.path = QLineEdit(background.path)
|
||||
self._source_background = background
|
||||
self._keep_embedded_image = background.has_embedded_image()
|
||||
self.path = QLineEdit(self.EMBEDDED_PATH_LABEL if self._keep_embedded_image else background.path)
|
||||
self.path.setPlaceholderText("PNG или JPEG")
|
||||
self.path.setReadOnly(self._keep_embedded_image)
|
||||
if self._keep_embedded_image:
|
||||
source_name = background.embedded_image_name or "изображение"
|
||||
self.path.setToolTip(f"{source_name} встроено в JSON схемы")
|
||||
browse = QPushButton("Выбрать")
|
||||
browse.clicked.connect(self._browse)
|
||||
path_row = QWidget()
|
||||
@ -69,14 +77,19 @@ class BackgroundDialog(QDialog):
|
||||
form.addRow(buttons)
|
||||
|
||||
def background(self) -> BackgroundImage:
|
||||
embedded = self._source_background if self._keep_embedded_image else BackgroundImage()
|
||||
return BackgroundImage(
|
||||
path=self.path.text().strip(),
|
||||
path="" if self._keep_embedded_image else self.path.text().strip(),
|
||||
offset_x=self.offset_x.value(),
|
||||
offset_y=self.offset_y.value(),
|
||||
scale_x_percent=self.scale_x.value(),
|
||||
scale_y_percent=self.scale_y.value(),
|
||||
opacity_percent=self.opacity.value(),
|
||||
visible=self.visible.isChecked(),
|
||||
embedded_image_base64=embedded.embedded_image_base64,
|
||||
embedded_image_mime_type=embedded.embedded_image_mime_type,
|
||||
embedded_image_name=embedded.embedded_image_name,
|
||||
embedded_image_sha256=embedded.embedded_image_sha256,
|
||||
)
|
||||
|
||||
def _reset_scale(self) -> None:
|
||||
@ -93,7 +106,13 @@ class BackgroundDialog(QDialog):
|
||||
"Изображения (*.png *.jpg *.jpeg);;Все файлы (*.*)",
|
||||
)
|
||||
if selected:
|
||||
self.path.setText(selected)
|
||||
self._set_external_path(selected)
|
||||
|
||||
def _set_external_path(self, path: str) -> None:
|
||||
self._keep_embedded_image = False
|
||||
self.path.setReadOnly(False)
|
||||
self.path.setToolTip("")
|
||||
self.path.setText(path)
|
||||
|
||||
@staticmethod
|
||||
def _spin(minimum: float, maximum: float, value: float, suffix: str) -> QDoubleSpinBox:
|
||||
|
||||
31
canvas.py
31
canvas.py
@ -3,7 +3,7 @@ from __future__ import annotations
|
||||
import math
|
||||
from pathlib import Path
|
||||
|
||||
from PySide6.QtCore import QPointF, QRectF, QSize, Qt, QTimer, Signal
|
||||
from PySide6.QtCore import QByteArray, QBuffer, QIODevice, QPointF, QRectF, QSize, Qt, QTimer, Signal
|
||||
from PySide6.QtGui import QColor, QBrush, QImage, QImageReader, QPainter, QPainterPath, QPen, QPixmap, QTransform
|
||||
from PySide6.QtWidgets import (
|
||||
QApplication,
|
||||
@ -93,7 +93,7 @@ class LedCanvas(QGraphicsView):
|
||||
self._background_handles: dict[str, QGraphicsRectItem] = {}
|
||||
self._background_edit_mode = False
|
||||
self._background_drag: dict | None = None
|
||||
self._background_pixmap_cache_key: tuple[str, int | None] | None = None
|
||||
self._background_pixmap_cache_key: tuple[str, str | int | None] | None = None
|
||||
self._background_pixmap_cache: QPixmap | None = None
|
||||
self._background_loaded = False
|
||||
self._direction_arrows_visible = True
|
||||
@ -532,10 +532,7 @@ class LedCanvas(QGraphicsView):
|
||||
self._remove_background_items()
|
||||
if self.document is None or not self.document.background.is_enabled():
|
||||
return
|
||||
image_path = Path(self.document.background.path)
|
||||
if not image_path.is_absolute() and self.document.path is not None:
|
||||
image_path = self.document.path.parent / image_path
|
||||
pixmap = self._load_background_pixmap(image_path)
|
||||
pixmap = self._load_background_pixmap(self.document.background)
|
||||
if pixmap.isNull():
|
||||
self._background_loaded = False
|
||||
return
|
||||
@ -555,16 +552,32 @@ class LedCanvas(QGraphicsView):
|
||||
self._background_handles.clear()
|
||||
self._background_loaded = False
|
||||
|
||||
def _load_background_pixmap(self, path: Path) -> QPixmap:
|
||||
cache_key = (str(path), path.stat().st_mtime_ns if path.exists() else None)
|
||||
def _load_background_pixmap(self, background: BackgroundImage) -> QPixmap:
|
||||
buffer: QBuffer | None = None
|
||||
if background.has_embedded_image():
|
||||
image_bytes = background.embedded_image_bytes()
|
||||
cache_key = ("embedded", background.embedded_image_sha256 or len(image_bytes))
|
||||
buffer = QBuffer()
|
||||
buffer.setData(QByteArray(image_bytes))
|
||||
buffer.open(QIODevice.OpenModeFlag.ReadOnly)
|
||||
reader = QImageReader(buffer)
|
||||
else:
|
||||
path = Path(background.path)
|
||||
if not path.is_absolute() and self.document is not None and self.document.path is not None:
|
||||
path = self.document.path.parent / path
|
||||
cache_key = (str(path), path.stat().st_mtime_ns if path.exists() else None)
|
||||
reader = QImageReader(str(path))
|
||||
if self._background_pixmap_cache_key == cache_key and self._background_pixmap_cache is not None:
|
||||
if buffer is not None:
|
||||
buffer.close()
|
||||
return self._background_pixmap_cache
|
||||
reader = QImageReader(str(path))
|
||||
size = reader.size()
|
||||
if size.isValid() and max(size.width(), size.height()) > MAX_BACKGROUND_PIXMAP_SIDE:
|
||||
factor = MAX_BACKGROUND_PIXMAP_SIDE / max(size.width(), size.height())
|
||||
reader.setScaledSize(QSize(max(1, int(size.width() * factor)), max(1, int(size.height() * factor))))
|
||||
image = reader.read()
|
||||
if buffer is not None:
|
||||
buffer.close()
|
||||
pixmap = QPixmap.fromImage(image) if not image.isNull() else QPixmap()
|
||||
self._background_pixmap_cache_key = cache_key
|
||||
self._background_pixmap_cache = pixmap
|
||||
|
||||
@ -968,15 +968,16 @@ class MainWindow(QMainWindow):
|
||||
if res != BackgroundDialog.Accepted:
|
||||
return
|
||||
background = dialog.background()
|
||||
if not background.path:
|
||||
return
|
||||
image_path = Path(background.path)
|
||||
if not image_path.is_absolute() and self.document.path is not None:
|
||||
image_path = self.document.path.parent / image_path
|
||||
reader = QImageReader(str(image_path))
|
||||
if not reader.canRead():
|
||||
show_warning(self, "Подложка не загружена", "Не удалось загрузить изображение подложки. Выберите PNG или JPEG файл.")
|
||||
if not background.is_enabled():
|
||||
return
|
||||
if not background.has_embedded_image():
|
||||
image_path = Path(background.path)
|
||||
if not image_path.is_absolute() and self.document.path is not None:
|
||||
image_path = self.document.path.parent / image_path
|
||||
reader = QImageReader(str(image_path))
|
||||
if not reader.canRead():
|
||||
show_warning(self, "Подложка не загружена", "Не удалось загрузить изображение подложки. Выберите PNG или JPEG файл.")
|
||||
return
|
||||
if background.to_payload() == self.document.background.to_payload():
|
||||
self.status.setText("Подложка не изменилась.")
|
||||
return
|
||||
|
||||
85
models.py
85
models.py
@ -1,12 +1,16 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import binascii
|
||||
import hashlib
|
||||
import json
|
||||
import math
|
||||
import mimetypes
|
||||
import os
|
||||
import re
|
||||
import tempfile
|
||||
from collections import deque
|
||||
from dataclasses import dataclass, field
|
||||
from dataclasses import dataclass, field, replace
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
@ -200,11 +204,33 @@ class BackgroundImage:
|
||||
scale_y_percent: float = 100.0
|
||||
opacity_percent: float = 100.0
|
||||
visible: bool = True
|
||||
embedded_image_base64: str = ""
|
||||
embedded_image_mime_type: str = ""
|
||||
embedded_image_name: str = ""
|
||||
embedded_image_sha256: str = ""
|
||||
|
||||
@classmethod
|
||||
def from_payload(cls, payload) -> "BackgroundImage": # noqa: ANN001
|
||||
if not isinstance(payload, dict):
|
||||
return cls()
|
||||
embedded = payload.get("embedded_image")
|
||||
embedded_data = ""
|
||||
embedded_mime_type = ""
|
||||
embedded_name = ""
|
||||
embedded_sha256 = ""
|
||||
if isinstance(embedded, dict):
|
||||
candidate_data = str(embedded.get("data") or "")
|
||||
try:
|
||||
decoded = base64.b64decode(candidate_data, validate=True) if candidate_data else b""
|
||||
except (binascii.Error, ValueError, TypeError):
|
||||
decoded = b""
|
||||
candidate_sha256 = str(embedded.get("sha256") or "").strip().lower()
|
||||
actual_sha256 = hashlib.sha256(decoded).hexdigest() if decoded else ""
|
||||
if decoded and (not candidate_sha256 or candidate_sha256 == actual_sha256):
|
||||
embedded_data = candidate_data
|
||||
embedded_mime_type = str(embedded.get("mime_type") or "application/octet-stream")
|
||||
embedded_name = Path(str(embedded.get("name") or "background")).name
|
||||
embedded_sha256 = actual_sha256
|
||||
scale = _optional_float(payload.get("scale_percent"))
|
||||
scale_x = _optional_float(payload.get("scale_x_percent"))
|
||||
scale_y = _optional_float(payload.get("scale_y_percent"))
|
||||
@ -223,10 +249,14 @@ class BackgroundImage:
|
||||
scale_y_percent=max(1.0, float(scale_y)),
|
||||
opacity_percent=min(100.0, max(0.0, 100.0 if opacity is None else float(opacity))),
|
||||
visible=bool(payload.get("visible", True)),
|
||||
embedded_image_base64=embedded_data,
|
||||
embedded_image_mime_type=embedded_mime_type,
|
||||
embedded_image_name=embedded_name,
|
||||
embedded_image_sha256=embedded_sha256,
|
||||
)
|
||||
|
||||
def to_payload(self) -> dict[str, bool | float | str]:
|
||||
return {
|
||||
def to_payload(self) -> dict[str, Any]:
|
||||
payload: dict[str, Any] = {
|
||||
"path": self.path,
|
||||
"offset_x": self.offset_x,
|
||||
"offset_y": self.offset_y,
|
||||
@ -235,9 +265,48 @@ class BackgroundImage:
|
||||
"opacity_percent": self.opacity_percent,
|
||||
"visible": self.visible,
|
||||
}
|
||||
if self.has_embedded_image():
|
||||
payload["embedded_image"] = {
|
||||
"encoding": "base64",
|
||||
"mime_type": self.embedded_image_mime_type or "application/octet-stream",
|
||||
"name": self.embedded_image_name or "background",
|
||||
"sha256": self.embedded_image_sha256 or hashlib.sha256(self.embedded_image_bytes()).hexdigest(),
|
||||
"data": self.embedded_image_base64,
|
||||
}
|
||||
return payload
|
||||
|
||||
def is_enabled(self) -> bool:
|
||||
return bool(self.path)
|
||||
return bool(self.path) or self.has_embedded_image()
|
||||
|
||||
def has_embedded_image(self) -> bool:
|
||||
return bool(self.embedded_image_base64)
|
||||
|
||||
def embedded_image_bytes(self) -> bytes:
|
||||
if not self.embedded_image_base64:
|
||||
return b""
|
||||
try:
|
||||
return base64.b64decode(self.embedded_image_base64, validate=True)
|
||||
except (binascii.Error, ValueError, TypeError):
|
||||
return b""
|
||||
|
||||
def with_embedded_image(self, base_dir: Path | None = None) -> "BackgroundImage":
|
||||
if self.has_embedded_image() or not self.path:
|
||||
return self
|
||||
image_path = Path(self.path)
|
||||
if not image_path.is_absolute() and base_dir is not None:
|
||||
image_path = base_dir / image_path
|
||||
image_bytes = image_path.read_bytes()
|
||||
if not image_bytes:
|
||||
raise ValueError("Файл подложки пуст и не может быть встроен в схему.")
|
||||
mime_type = mimetypes.guess_type(image_path.name)[0] or "application/octet-stream"
|
||||
return replace(
|
||||
self,
|
||||
path="",
|
||||
embedded_image_base64=base64.b64encode(image_bytes).decode("ascii"),
|
||||
embedded_image_mime_type=mime_type,
|
||||
embedded_image_name=image_path.name,
|
||||
embedded_image_sha256=hashlib.sha256(image_bytes).hexdigest(),
|
||||
)
|
||||
|
||||
|
||||
class MappingDocument:
|
||||
@ -397,7 +466,9 @@ class MappingDocument:
|
||||
def save(self, path: str | Path) -> None:
|
||||
path = Path(path)
|
||||
self.reindex_all()
|
||||
payload = self.to_payload()
|
||||
source_base_dir = self.path.parent if self.path is not None else path.parent
|
||||
background_for_save = self.background.with_embedded_image(source_base_dir)
|
||||
payload = self.to_payload(background=background_for_save)
|
||||
serialized = json.dumps(payload, ensure_ascii=False, indent=2, allow_nan=False)
|
||||
if json.loads(serialized) != payload:
|
||||
raise ValueError("Не удалось без потерь сериализовать схему.")
|
||||
@ -422,6 +493,7 @@ class MappingDocument:
|
||||
if temporary_path is not None and temporary_path.exists():
|
||||
temporary_path.unlink()
|
||||
self.path = path
|
||||
self.background = background_for_save
|
||||
|
||||
def to_payload(
|
||||
self,
|
||||
@ -429,6 +501,7 @@ class MappingDocument:
|
||||
point_payloads: list[dict[str, Any]] | None = None,
|
||||
lines_payload: list[dict[str, Any]] | None = None,
|
||||
collection_name: str | None = None,
|
||||
background: BackgroundImage | None = None,
|
||||
) -> dict[str, Any]:
|
||||
payload = dict(self.extra_payload)
|
||||
payload[collection_name or self.collection_name] = (
|
||||
@ -439,7 +512,7 @@ class MappingDocument:
|
||||
payload["unfolding"] = self.describe_unfolding().to_payload()
|
||||
payload["coordinate_unit"] = self._coordinate_unit_for_save()
|
||||
payload["grid_step_mm"] = self.grid_step_mm
|
||||
payload["background"] = self.background.to_payload()
|
||||
payload["background"] = (self.background if background is None else background).to_payload()
|
||||
payload["lines"] = self.lines_to_payload() if lines_payload is None else lines_payload
|
||||
return payload
|
||||
|
||||
|
||||
122
qa_smoke.py
122
qa_smoke.py
@ -1,5 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import hashlib
|
||||
import json
|
||||
import importlib
|
||||
import gc
|
||||
@ -147,7 +149,10 @@ def main() -> None:
|
||||
test_save_preserves_unknown_source_fields,
|
||||
test_ordered_json_preserves_document_and_remaps_uids,
|
||||
test_background_load_save_roundtrip,
|
||||
test_background_image_is_embedded_and_survives_source_changes,
|
||||
test_background_embedding_failure_keeps_document_and_target_unchanged,
|
||||
test_background_dialog_preserves_non_uniform_scale,
|
||||
test_background_dialog_marks_and_preserves_embedded_image,
|
||||
test_canvas_background_layer_and_geometry,
|
||||
test_canvas_background_zero_offset_uses_scheme_center,
|
||||
test_canvas_background_edit_move_and_resize_helpers,
|
||||
@ -1620,6 +1625,11 @@ def test_background_load_save_roundtrip() -> None:
|
||||
}
|
||||
source = Path(temp_dir) / "map.json"
|
||||
source.write_text(json.dumps(payload), encoding="utf-8")
|
||||
(Path(temp_dir) / "drawing.png").write_bytes(
|
||||
base64.b64decode(
|
||||
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII="
|
||||
)
|
||||
)
|
||||
doc = MappingDocument()
|
||||
doc.load(source)
|
||||
assert doc.background.path == "drawing.png"
|
||||
@ -1652,6 +1662,81 @@ def test_background_load_save_roundtrip() -> None:
|
||||
assert reloaded.background.offset_y == 0
|
||||
|
||||
|
||||
def test_background_image_is_embedded_and_survives_source_changes() -> None:
|
||||
if LedCanvas is None or QApplication is None or QImage is None:
|
||||
return
|
||||
app = QApplication.instance() or QApplication([])
|
||||
with temp_document_path() as path, tempfile.TemporaryDirectory() as temp_dir:
|
||||
image_path = Path(temp_dir) / "background.png"
|
||||
image = QImage(20, 10, QImage.Format.Format_ARGB32)
|
||||
image.fill(QColor("#123456"))
|
||||
assert image.save(str(image_path))
|
||||
original_bytes = image_path.read_bytes()
|
||||
|
||||
doc = MappingDocument()
|
||||
doc.load(path)
|
||||
doc.background = BackgroundImage(str(image_path), 12.5, -7.25, 200.0, 300.0, 45.0, True)
|
||||
saved = Path(temp_dir) / "embedded-map.json"
|
||||
doc.save(saved)
|
||||
|
||||
payload = json.loads(saved.read_text(encoding="utf-8"))
|
||||
background_payload = payload["background"]
|
||||
embedded_payload = background_payload["embedded_image"]
|
||||
assert background_payload["path"] == ""
|
||||
assert embedded_payload["encoding"] == "base64"
|
||||
assert embedded_payload["mime_type"] == "image/png"
|
||||
assert embedded_payload["name"] == "background.png"
|
||||
assert embedded_payload["sha256"] == hashlib.sha256(original_bytes).hexdigest()
|
||||
assert base64.b64decode(embedded_payload["data"], validate=True) == original_bytes
|
||||
assert doc.background.has_embedded_image()
|
||||
assert doc.background.path == ""
|
||||
|
||||
image_path.unlink()
|
||||
first_embedded_data = doc.background.embedded_image_base64
|
||||
doc.points[0].x += 1000.0
|
||||
doc.save(saved)
|
||||
assert doc.background.embedded_image_base64 == first_embedded_data
|
||||
|
||||
reloaded = MappingDocument()
|
||||
reloaded.load(saved)
|
||||
assert reloaded.background.to_payload() == doc.background.to_payload()
|
||||
canvas = LedCanvas()
|
||||
canvas.set_document(reloaded)
|
||||
assert canvas.background_loaded()
|
||||
assert canvas._background_item is not None
|
||||
assert canvas._background_item.pixmap().width() == 20
|
||||
assert canvas._background_item.pixmap().height() == 10
|
||||
rect = canvas._background_scene_rect()
|
||||
assert rect is not None
|
||||
assert rect.width() == 40.0
|
||||
assert rect.height() == 30.0
|
||||
canvas.close()
|
||||
|
||||
|
||||
def test_background_embedding_failure_keeps_document_and_target_unchanged() -> None:
|
||||
with temp_document_path() as source_path, tempfile.TemporaryDirectory() as temp_dir:
|
||||
doc = MappingDocument()
|
||||
doc.load(source_path)
|
||||
missing_path = Path(temp_dir) / "missing-background.png"
|
||||
original_background = BackgroundImage(path=str(missing_path), offset_x=12.0, scale_x_percent=150.0)
|
||||
doc.background = original_background
|
||||
target = Path(temp_dir) / "existing-map.json"
|
||||
original_target = '{"preserve": true}\n'
|
||||
target.write_text(original_target, encoding="utf-8")
|
||||
|
||||
try:
|
||||
doc.save(target)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
else:
|
||||
raise AssertionError("Сохранение должно завершаться ошибкой, если файл подложки недоступен.")
|
||||
|
||||
assert target.read_text(encoding="utf-8") == original_target
|
||||
assert doc.path == source_path
|
||||
assert doc.background is original_background
|
||||
assert not doc.background.has_embedded_image()
|
||||
|
||||
|
||||
def test_background_dialog_preserves_non_uniform_scale() -> None:
|
||||
if QApplication is None or BackgroundDialog is None:
|
||||
return
|
||||
@ -1665,6 +1750,43 @@ def test_background_dialog_preserves_non_uniform_scale() -> None:
|
||||
dialog.close()
|
||||
|
||||
|
||||
def test_background_dialog_marks_and_preserves_embedded_image() -> None:
|
||||
if QApplication is None or BackgroundDialog is None:
|
||||
return
|
||||
app = QApplication.instance() or QApplication([])
|
||||
image_bytes = b"embedded-background-image"
|
||||
encoded = base64.b64encode(image_bytes).decode("ascii")
|
||||
background = BackgroundImage(
|
||||
path="",
|
||||
offset_x=1.0,
|
||||
offset_y=2.0,
|
||||
scale_x_percent=80.0,
|
||||
scale_y_percent=125.0,
|
||||
opacity_percent=60.0,
|
||||
visible=True,
|
||||
embedded_image_base64=encoded,
|
||||
embedded_image_mime_type="image/png",
|
||||
embedded_image_name="source.png",
|
||||
embedded_image_sha256=hashlib.sha256(image_bytes).hexdigest(),
|
||||
)
|
||||
dialog = BackgroundDialog(background)
|
||||
assert dialog.path.text() == "Встроенная"
|
||||
assert dialog.path.isReadOnly()
|
||||
dialog.scale_x.setValue(90.0)
|
||||
updated = dialog.background()
|
||||
assert updated.path == ""
|
||||
assert updated.scale_x_percent == 90.0
|
||||
assert updated.embedded_image_base64 == encoded
|
||||
assert updated.embedded_image_sha256 == background.embedded_image_sha256
|
||||
|
||||
dialog._set_external_path("replacement.jpg")
|
||||
replacement = dialog.background()
|
||||
assert not dialog.path.isReadOnly()
|
||||
assert replacement.path == "replacement.jpg"
|
||||
assert not replacement.has_embedded_image()
|
||||
dialog.close()
|
||||
|
||||
|
||||
def test_canvas_background_layer_and_geometry() -> None:
|
||||
if LedCanvas is None or QApplication is None or QImage is None:
|
||||
return
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user