From 479665b9f155e734251f70bd5a626f41061c5b70 Mon Sep 17 00:00:00 2001 From: Rebecca Breu Date: Sun, 19 May 2024 19:05:31 +0200 Subject: [PATCH] Export all images from scene --- CHANGELOG.rst | 1 + beeref/actions/actions.py | 6 + beeref/actions/menu_structure.py | 1 + beeref/fileio/__init__.py | 1 + beeref/fileio/export.py | 159 ++++-- beeref/fileio/sql.py | 8 +- beeref/items.py | 11 + beeref/view.py | 41 +- beeref/widgets/__init__.py | 42 +- tests/fileio/test_export.py | 451 ------------------ .../fileio/test_export_images_to_directory.py | 287 +++++++++++ tests/fileio/test_export_scene_to_pixmap.py | 181 +++++++ tests/fileio/test_export_scene_to_svg.py | 283 +++++++++++ tests/items/test_pixmapitem.py | 37 ++ tests/test_view.py | 94 ++++ tests/widgets/test_widgets.py | 13 + 16 files changed, 1122 insertions(+), 494 deletions(-) create mode 100644 tests/fileio/test_export_images_to_directory.py create mode 100644 tests/fileio/test_export_scene_to_pixmap.py create mode 100644 tests/fileio/test_export_scene_to_svg.py diff --git a/CHANGELOG.rst b/CHANGELOG.rst index e06b6a2..8947cc2 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -9,6 +9,7 @@ Added setting can be overridden by Qt's default environment variable QT_IMAGEIO_MAXALLOC * Display error messages when images can't be loaded from bee files +* Added option to export all images from scene (File -> Export Images) 0.3.3 - 2024-05-05 diff --git a/beeref/actions/actions.py b/beeref/actions/actions.py index 6e45dd9..5dab777 100644 --- a/beeref/actions/actions.py +++ b/beeref/actions/actions.py @@ -141,6 +141,12 @@ actions = ActionList([ callback='on_action_export_scene', group='active_when_items_in_scene', ), + Action( + id='export_images', + text='Export &Images...', + callback='on_action_export_images', + group='active_when_items_in_scene', + ), Action( id='quit', text='&Quit', diff --git a/beeref/actions/menu_structure.py b/beeref/actions/menu_structure.py index 7c3a35b..4428e77 100644 --- a/beeref/actions/menu_structure.py +++ b/beeref/actions/menu_structure.py @@ -29,6 +29,7 @@ menu_structure = [ 'save', 'save_as', 'export_scene', + 'export_images', MENU_SEPARATOR, 'quit', ], diff --git a/beeref/fileio/__init__.py b/beeref/fileio/__init__.py index be90000..4307ca9 100644 --- a/beeref/fileio/__init__.py +++ b/beeref/fileio/__init__.py @@ -87,6 +87,7 @@ class ThreadedIO(QtCore.QThread): progress = QtCore.pyqtSignal(int) finished = QtCore.pyqtSignal(str, list) begin_processing = QtCore.pyqtSignal(int) + user_input_required = QtCore.pyqtSignal(str) def __init__(self, func, *args, **kwargs): super().__init__() diff --git a/beeref/fileio/export.py b/beeref/fileio/export.py index 3afd15c..69267b3 100644 --- a/beeref/fileio/export.py +++ b/beeref/fileio/export.py @@ -15,12 +15,14 @@ import base64 import logging +import pathlib from xml.etree import ElementTree as ET from PyQt6 import QtCore, QtGui from .errors import BeeFileIOError from beeref import constants, widgets +from beeref.items import BeePixmapItem logger = logging.getLogger(__name__) @@ -46,6 +48,36 @@ def register_exporter(cls): class ExporterBase: + + def emit_begin_processing(self, worker, start): + if worker: + worker.begin_processing.emit(start) + + def emit_progress(self, worker, progress): + if worker: + worker.progress.emit(progress) + + def emit_finished(self, worker, filename, errors): + filename = str(filename) + if worker: + worker.finished.emit(filename, errors) + + def emit_user_input_required(self, worker, msg): + if worker: + worker.user_input_required.emit(msg) + + def handle_export_error(self, filename, error, worker): + filename = str(filename) + logger.debug(f'Export failed: {error}') + if worker: + worker.finished.emit(filename, [str(error)]) + return + else: + e = error if isinstance(error, Exception) else None + raise BeeFileIOError(msg=str(error), filename=filename) from e + + +class SceneExporterBase(ExporterBase): """For exporting the scene to a single image.""" def __init__(self, scene): @@ -67,7 +99,7 @@ class ExporterBase: @register_exporter -class SceneToPixmapExporter(ExporterBase): +class SceneToPixmapExporter(SceneExporterBase): TYPE = ExporterRegistry.DEFAULT_TYPE @@ -108,33 +140,25 @@ class SceneToPixmapExporter(ExporterBase): def export(self, filename, worker=None): logger.debug(f'Exporting scene to {filename}') - if worker: - worker.begin_processing.emit(1) - + self.emit_begin_processing(worker, 1) image = self.render_to_image() if worker and worker.canceled: logger.debug('Export canceled') - worker.finished.emit(filename, []) + self.emit_finished(worker, filename, []) return if not image.save(filename, quality=90): - msg = 'Error writing file' - logger.debug(f'Export failed: {msg}') - if worker: - worker.finished.emit(filename, [msg]) - return - else: - raise BeeFileIOError(msg, filename=filename) + self.handle_export_error(filename, 'Error writing file', worker) + return logger.debug('Export finished') - if worker: - worker.progress.emit(1) - worker.finished.emit(filename, []) + self.emit_progress(worker, 1) + self.emit_finished(worker, filename, []) @register_exporter -class SceneToSVGExporter(ExporterBase): +class SceneToSVGExporter(SceneExporterBase): TYPE = 'svg' @@ -222,18 +246,15 @@ class SceneToSVGExporter(ExporterBase): element.set('opacity', str(item.opacity())) svg.append(element) - if worker: - worker.progress.emit(i) - if worker.canceled: - return + self.emit_progress(worker, i) + if worker and worker.canceled: + return return svg def export(self, filename, worker=None): logger.debug(f'Exporting scene to {filename}') - if worker: - worker.begin_processing.emit(len(self.scene.items())) - + self.emit_begin_processing(worker, len(self.scene.items())) svg = self.render_to_svg(worker) if worker and worker.canceled: @@ -248,13 +269,89 @@ class SceneToSVGExporter(ExporterBase): with open(filename, 'w') as f: tree.write(f, encoding='unicode', xml_declaration=True) except OSError as e: - logger.debug(f'Export failed: {e}') - if worker: - worker.finished.emit(filename, [str(e)]) - return - else: - raise BeeFileIOError(msg=str(e), filename=filename) from e + self.handle_export_error(filename, e, worker) + return logger.debug('Export finished') - if worker: - worker.finished.emit(filename, []) + self.emit_finished(worker, filename, []) + + +class ImagesToDirectoryExporter(ExporterBase): + """Export all images to a folder. + + Not registered in the registry as it is accessed via its own menu entry, + not auto-detected by file extension. + """ + + def __init__(self, scene, dirname): + self.scene = scene + self.dirname = dirname + self.items = list(self.scene.items_by_type(BeePixmapItem.TYPE)) + self.max_save_id = 0 + for item in self.items: + if item.save_id: + self.max_save_id = max(self.max_save_id, item.save_id) + self.num_total = len(self.items) + self.start_from = 0 + self.handle_existing = None + + def export(self, worker=None): + logger.debug(f'Exporting images to {self.dirname}') + logger.debug(f'Starting at {self.start_from}') + + self.emit_begin_processing(worker, self.num_total) + self.emit_progress(worker, self.start_from) + + for i, item in enumerate( + self.items[self.start_from:], start=self.start_from): + if worker and worker.canceled: + logger.debug('Export canceled') + worker.finished.emit(self.dirname, []) + return + + pixmap, imgformat = item.pixmap_to_bytes() + + if item.save_id: + filename = item.get_filename_for_export(imgformat) + else: + self.max_save_id += 1 + save_id = self.max_save_id + filename = item.get_filename_for_export(imgformat, save_id) + + try: + path = pathlib.Path(self.dirname) / filename + path_exists = path.exists() + except OSError as e: + self.handle_export_error(self.dirname, e, worker) + return + + if path_exists: + logger.debug(f'File already exists: {path}') + if self.handle_existing is None: + self.start_from = i + self.emit_user_input_required(worker, str(path)) + return + else: + if self.handle_existing == 'skip': + self.handle_existing = None + logger.debug('Skipping file') + continue + elif self.handle_existing == 'skip_all': + logger.debug('Skipping file') + continue + elif self.handle_existing == 'overwrite': + self.handle_existing = None + logger.debug('Overwrite file') + elif self.handle_existing == 'overwrite_all': + logger.debug('Overwrite file') + + logger.debug(f'Writing file: {path}') + try: + path.write_bytes(pixmap) + except OSError as e: + self.handle_export_error(path, e, worker) + return + + self.emit_progress(worker, i) + + self.emit_finished(worker, self.dirname, []) diff --git a/beeref/fileio/sql.py b/beeref/fileio/sql.py index 2107ee8..f4c7b5f 100644 --- a/beeref/fileio/sql.py +++ b/beeref/fileio/sql.py @@ -296,13 +296,7 @@ class SQLiteIO: if hasattr(item, 'pixmap_to_bytes'): pixmap, imgformat = item.pixmap_to_bytes() - - if item.filename: - basename = os.path.splitext(os.path.basename(item.filename))[0] - name = f'{item.save_id:04}-{basename}.{imgformat}' - else: - name = f'{item.save_id:04}.{imgformat}' - + name = item.get_filename_for_export(imgformat) self.ex( 'INSERT INTO sqlar (item_id, name, mode, sz, data) ' 'VALUES (?, ?, ?, ?, ?)', diff --git a/beeref/items.py b/beeref/items.py index 995aeab..7a2c45b 100644 --- a/beeref/items.py +++ b/beeref/items.py @@ -20,6 +20,7 @@ text). from collections import defaultdict from functools import cached_property import logging +import os.path from PyQt6 import QtCore, QtGui, QtWidgets from PyQt6.QtCore import Qt @@ -197,6 +198,16 @@ class BeePixmapItem(BeeItemMixin, QtWidgets.QGraphicsPixmapItem): self.crop.width(), self.crop.height()]} + def get_filename_for_export(self, imgformat, save_id_default=None): + save_id = self.save_id or save_id_default + assert save_id is not None + + if self.filename: + basename = os.path.splitext(os.path.basename(self.filename))[0] + return f'{save_id:04}-{basename}.{imgformat}' + else: + return f'{save_id:04}.{imgformat}' + def get_imgformat(self, img): """Determines the format for storing this image.""" diff --git a/beeref/view.py b/beeref/view.py index 31b22e3..dcfb8a0 100644 --- a/beeref/view.py +++ b/beeref/view.py @@ -27,7 +27,7 @@ from beeref.config import CommandlineArgs, BeeSettings, KeyboardSettings from beeref import constants from beeref import fileio from beeref.fileio.errors import IMG_LOADING_ERROR_MSG -from beeref.fileio.export import exporter_registry +from beeref.fileio.export import exporter_registry, ImagesToDirectoryExporter from beeref import widgets from beeref.items import BeePixmapItem, BeeTextItem from beeref.main_controls import MainControlsMixin @@ -396,7 +396,7 @@ class BeeGraphicsView(MainControlsMixin, self.worker.progress.connect(self.on_items_loaded) self.worker.finished.connect(self.on_loading_finished) self.progress = widgets.BeeProgressDialog( - 'Loading %s' % filename, + f'Loading {filename}', worker=self.worker, parent=self) self.worker.start() @@ -430,7 +430,7 @@ class BeeGraphicsView(MainControlsMixin, fileio.save_bee, filename, self.scene, create_new=create_new) self.worker.finished.connect(self.on_saving_finished) self.progress = widgets.BeeProgressDialog( - 'Saving %s' % filename, + f'Saving {filename}', worker=self.worker, parent=self) self.worker.start() @@ -481,7 +481,7 @@ class BeeGraphicsView(MainControlsMixin, self.worker = fileio.ThreadedIO(exporter.export, filename) self.worker.finished.connect(self.on_export_finished) self.progress = widgets.BeeProgressDialog( - 'Exporting %s' % filename, + f'Exporting {filename}', worker=self.worker, parent=self) self.worker.start() @@ -494,6 +494,39 @@ class BeeGraphicsView(MainControlsMixin, 'Problem writing file', f'

Problem writing file {filename}

{err_msg}

') + def on_action_export_images(self): + directory = os.path.dirname(self.filename) if self.filename else None + directory = QtWidgets.QFileDialog.getExistingDirectory( + parent=self, + caption='Export Images', + directory=directory) + + if not directory: + return + + logger.debug(f'Got export directory {directory}') + self.exporter = ImagesToDirectoryExporter(self.scene, directory) + self.worker = fileio.ThreadedIO(self.exporter.export) + self.worker.user_input_required.connect( + self.on_export_images_file_exists) + self.worker.finished.connect(self.on_export_finished) + self.progress = widgets.BeeProgressDialog( + f'Exporting to {directory}', + worker=self.worker, + parent=self) + self.worker.start() + + def on_export_images_file_exists(self, filename): + dlg = widgets.ExportImagesFileExistsDialog(self, filename) + if dlg.exec() == QtWidgets.QDialog.DialogCode.Accepted: + self.exporter.handle_existing = dlg.get_answer() + directory = self.exporter.dirname + self.progress = widgets.BeeProgressDialog( + f'Exporting to {directory}', + worker=self.worker, + parent=self) + self.worker.start() + def on_action_quit(self): logger.info('User quit. Exiting...') self.app.quit() diff --git a/beeref/widgets/__init__.py b/beeref/widgets/__init__.py index a5d0b4f..9ef8564 100644 --- a/beeref/widgets/__init__.py +++ b/beeref/widgets/__init__.py @@ -44,6 +44,7 @@ class BeeProgressDialog(QtWidgets.QProgressDialog): worker.begin_processing.connect(self.on_begin_processing) worker.progress.connect(self.on_progress) worker.finished.connect(self.on_finished) + worker.user_input_required.connect(self.on_finished) self.canceled.connect(worker.on_canceled) def on_progress(self, value): @@ -54,7 +55,7 @@ class BeeProgressDialog(QtWidgets.QProgressDialog): logger.debug(f'Beginn progress dialog: {value}') self.setMaximum(value) - def on_finished(self, filename, errors): + def on_finished(self, *args, **kwargs): logger.debug('Finished progress dialog') self.setValue(self.maximum()) self.reset() @@ -296,3 +297,42 @@ class SampleColorWidget(QtWidgets.QWidget): self.set_pos(pos) self.color = color self.repaint() + + +class ExportImagesFileExistsDialog(QtWidgets.QDialog): + + def __init__(self, parent, filename): + super().__init__(parent) + self.setWindowTitle('File exists') + + layout = QtWidgets.QVBoxLayout() + self.setLayout(layout) + + label = QtWidgets.QLabel( + f'File already exists:\n{filename}') + layout.addWidget(label) + + choices = (('skip', 'Skip this file'), + ('skip_all', 'Skip all existing files'), + ('overwrite', 'Overwrite this file'), + ('overwrite_all', 'Overwrite all existing files')) + + self.radio_buttons = {} + for (value, label) in choices: + btn = QtWidgets.QRadioButton(label) + self.radio_buttons[value] = btn + layout.addWidget(btn) + self.radio_buttons['skip'].setChecked(True) + + # Bottom row of buttons + buttons = QtWidgets.QDialogButtonBox( + QtWidgets.QDialogButtonBox.StandardButton.Ok + | QtWidgets.QDialogButtonBox.StandardButton.Cancel) + buttons.rejected.connect(self.reject) + buttons.accepted.connect(self.accept) + layout.addWidget(buttons) + + def get_answer(self): + for value, btn in self.radio_buttons.items(): + if btn.isChecked(): + return value diff --git a/tests/fileio/test_export.py b/tests/fileio/test_export.py index 5314cca..e099b0a 100644 --- a/tests/fileio/test_export.py +++ b/tests/fileio/test_export.py @@ -1,13 +1,5 @@ -import os -import stat -from unittest.mock import patch, ANY, MagicMock import pytest -from PyQt6 import QtGui, QtCore - -from beeref import constants -from beeref.items import BeePixmapItem, BeeTextItem -from beeref.fileio.errors import BeeFileIOError from beeref.fileio.export import ( exporter_registry, SceneToPixmapExporter, @@ -21,446 +13,3 @@ from beeref.fileio.export import ( ('svg', SceneToSVGExporter)]) def test_registry(key, expected): exporter_registry[key] == expected - - -@patch('beeref.widgets.SceneToPixmapExporterDialog.exec', return_value=True) -@patch('beeref.widgets.SceneToPixmapExporterDialog.value', - return_value=QtCore.QSize(100, 200)) -def test_scene_to_pixmap_exporter_get_user_input(value_mock, exec_mock, view): - exporter = SceneToPixmapExporter(view.scene) - value = exporter.get_user_input(None) - assert value is True - assert exporter.size == QtCore.QSize(100, 200) - - -@patch('beeref.widgets.SceneToPixmapExporterDialog.exec', return_value=False) -@patch('beeref.widgets.SceneToPixmapExporterDialog.value') -def test_scene_to_pixmap_exporter_get_user_input_when_canceled( - value_mock, exec_mock, view): - exporter = SceneToPixmapExporter(view.scene) - value = exporter.get_user_input(None) - assert value is False - - -def test_scene_to_pixmap_exporter_default_size_and_margin(view): - item1 = BeePixmapItem( - QtGui.QImage(100, 100, QtGui.QImage.Format.Format_RGB32)) - item1.setPos(QtCore.QPointF(0, 0)) - view.scene.addItem(item1) - - item2 = BeePixmapItem( - QtGui.QImage(100, 100, QtGui.QImage.Format.Format_RGB32)) - item1.setPos(QtCore.QPointF(200, 0)) - view.scene.addItem(item2) - - exporter = SceneToPixmapExporter(view.scene) - assert view.scene.sceneRect().size().toSize() == QtCore.QSize(300, 100) - assert (exporter.margin - 9) < 0.000001 - assert exporter.default_size == QtCore.QSize(318, 118) - - -def test_scene_to_pixmap_exporter_default_size_and_margin_when_selection(view): - item1 = BeePixmapItem( - QtGui.QImage(100, 100, QtGui.QImage.Format.Format_RGB32)) - item1.setPos(QtCore.QPointF(0, 0)) - view.scene.addItem(item1) - - item2 = BeePixmapItem( - QtGui.QImage(100, 100, QtGui.QImage.Format.Format_RGB32)) - item1.setPos(QtCore.QPointF(200, 0)) - view.scene.addItem(item2) - item2.setSelected(True) - - exporter = SceneToPixmapExporter(view.scene) - assert view.scene.sceneRect().size().toSize() == QtCore.QSize(300, 100) - assert (exporter.margin - 9) < 0.000001 - assert exporter.default_size == QtCore.QSize(318, 118) - - -@patch('beeref.scene.BeeGraphicsScene.render') -def test_scene_to_pixmap_exporter_render_sets_margins(render_mock, view): - item = BeePixmapItem( - QtGui.QImage(1000, 1200, QtGui.QImage.Format.Format_RGB32)) - view.scene.addItem(item) - exporter = SceneToPixmapExporter(view.scene) - assert exporter.margin == 36 - assert exporter.default_size == QtCore.QSize(1072, 1272) - - exporter.size = QtCore.QSize(536, 636) - exporter.render_to_image() - - render_mock.assert_called_once_with( - ANY, - source=QtCore.QRectF(0, 0, 1000, 1200), - target=QtCore.QRectF(18, 18, 500, 600)) - - -def test_scene_to_pixmap_exporter_render_renders_scene(view): - item_img = QtGui.QImage(1000, 1200, QtGui.QImage.Format.Format_RGB32) - item_img.fill(QtGui.QColor(11, 22, 33)) - item = BeePixmapItem(item_img) - view.scene.addItem(item) - exporter = SceneToPixmapExporter(view.scene) - assert exporter.margin == 36 - assert exporter.default_size == QtCore.QSize(1072, 1272) - - exporter.size = QtCore.QSize(536, 636) - image = exporter.render_to_image() - assert image.pixel(1, 1) == QtGui.QColor(*constants.COLORS['Scene:Canvas']) - assert image.pixel(100, 100) == QtGui.QColor(11, 22, 33) - - -def test_scene_to_pixmap_exporter_export_writes_image(view, tmpdir): - filename = os.path.join(tmpdir, 'foo.png') - item_img = QtGui.QImage(1000, 1200, QtGui.QImage.Format.Format_RGB32) - item = BeePixmapItem(item_img) - view.scene.addItem(item) - exporter = SceneToPixmapExporter(view.scene) - exporter.size = QtCore.QSize(100, 120) - exporter.export(filename) - - with open(filename, 'rb') as f: - assert f.read().startswith(b'\x89PNG') - - -def test_scene_to_pixmap_exporter_export_with_worker(view, tmpdir): - filename = os.path.join(tmpdir, 'foo.png') - item_img = QtGui.QImage(1000, 1200, QtGui.QImage.Format.Format_RGB32) - item = BeePixmapItem(item_img) - view.scene.addItem(item) - exporter = SceneToPixmapExporter(view.scene) - exporter.size = QtCore.QSize(100, 120) - worker = MagicMock(canceled=False) - exporter.export(filename, worker) - - worker.begin_processing.emit.assert_called_once_with(1) - worker.progress.emit.assert_called_once_with(1) - worker.finished.emit.assert_called_once_with(filename, []) - with open(filename, 'rb') as f: - assert f.read().startswith(b'\x89PNG') - - -def test_scene_to_pixmap_exporter_export_with_worker_when_canceled( - view, tmpdir): - filename = os.path.join(tmpdir, 'foo.png') - item_img = QtGui.QImage(1000, 1200, QtGui.QImage.Format.Format_RGB32) - item = BeePixmapItem(item_img) - view.scene.addItem(item) - exporter = SceneToPixmapExporter(view.scene) - exporter.size = QtCore.QSize(100, 120) - worker = MagicMock(canceled=True) - exporter.export(filename, worker) - - worker.begin_processing.emit.assert_called_once_with(1) - worker.progress.emit.assert_not_called() - worker.finished.emit.assert_called_once_with(filename, []) - os.path.exists(filename) is False - - -def test_scene_to_pixmap_exporter_export_when_file_not_writeable(view, tmpdir): - filename = os.path.join(tmpdir, 'foo.png') - with open(filename, 'w') as f: - f.write('foo') - os.chmod(filename, stat.S_IREAD) - item_img = QtGui.QImage(1000, 1200, QtGui.QImage.Format.Format_RGB32) - item = BeePixmapItem(item_img) - view.scene.addItem(item) - exporter = SceneToPixmapExporter(view.scene) - exporter.size = QtCore.QSize(100, 120) - - with pytest.raises(BeeFileIOError) as e: - exporter.export(filename) - assert e.filename == filename - - -def test_scene_to_pixmap_exporter_export_when_file_not_writeable_with_worker( - view, tmpdir): - filename = os.path.join(tmpdir, 'foo.png') - with open(filename, 'w') as f: - f.write('foo') - os.chmod(filename, stat.S_IREAD) - item_img = QtGui.QImage(1000, 1200, QtGui.QImage.Format.Format_RGB32) - item = BeePixmapItem(item_img) - view.scene.addItem(item) - exporter = SceneToPixmapExporter(view.scene) - exporter.size = QtCore.QSize(100, 120) - worker = MagicMock(canceled=False) - - exporter.export(filename, worker=worker) - worker.begin_processing.emit.assert_called_once_with(1) - worker.progress.emit.assert_not_called() - worker.finished.emit.assert_called_once_with( - filename, ['Error writing file']) - - -def test_scene_to_svg_exporter_get_user_input(view): - item1 = BeePixmapItem( - QtGui.QImage(100, 100, QtGui.QImage.Format.Format_RGB32)) - item1.setPos(QtCore.QPointF(0, 0)) - view.scene.addItem(item1) - - item2 = BeePixmapItem( - QtGui.QImage(100, 100, QtGui.QImage.Format.Format_RGB32)) - item1.setPos(QtCore.QPointF(200, 0)) - view.scene.addItem(item2) - - assert view.scene.sceneRect().size().toSize() == QtCore.QSize(300, 100) - exporter = SceneToSVGExporter(view.scene) - value = exporter.get_user_input(None) - assert value is True - assert exporter.size == QtCore.QSize(318, 118) - - -def test_scene_to_svg_exporter_render_pixmap_items(view): - item1 = BeePixmapItem( - QtGui.QImage(100, 110, QtGui.QImage.Format.Format_RGB32)) - item1.setPos(QtCore.QPointF(20, 30)) - view.scene.addItem(item1) - - item2 = BeePixmapItem( - QtGui.QImage(70, 77, QtGui.QImage.Format.Format_RGB32)) - item2.setPos(QtCore.QPointF(50, 50)) - item2.setZValue(-1) - view.scene.addItem(item2) - - exporter = SceneToSVGExporter(view.scene) - exporter.size = QtCore.QSize(200, 400) - exporter.margin = 5 - svg = exporter.render_to_svg() - - assert svg.tag == 'svg' - assert svg.get('width') == '200' - assert svg.get('height') == '400' - assert svg.get('xmlns') == 'http://www.w3.org/2000/svg' - assert svg.get('xmlns:xlink') == 'http://www.w3.org/1999/xlink' - assert len(svg) == 2 - - element = svg[0] # item2 - assert element.tag == 'image' - assert element.get('xlink:href').startswith('data:image/png;base64,iVBOR') - assert element.get('width') == '70.0' - assert element.get('height') == '77.0' - assert element.get('image-rendering') == 'optimizeQuality' - assert element.get('transform') == 'rotate(0.0 35.0 25.0)' - assert element.get('x') == '35.0' - assert element.get('y') == '25.0' - assert element.get('opacity') == '1.0' - - element = svg[1] # item1 - assert element.tag == 'image' - assert element.get('width') == '100.0' - assert element.get('height') == '110.0' - assert element.get('image-rendering') == 'optimizeQuality' - assert element.get('transform') == 'rotate(0.0 5.0 5.0)' - assert element.get('x') == '5.0' - assert element.get('y') == '5.0' - assert element.get('opacity') == '1.0' - - -def test_scene_to_svg_exporter_render_pixmap_with_crop(view): - item = BeePixmapItem( - QtGui.QImage(100, 110, QtGui.QImage.Format.Format_RGB32)) - item.setPos(QtCore.QPointF(20, 30)) - item.crop = QtCore.QRectF(20, 25, 30, 33) - view.scene.addItem(item) - exporter = SceneToSVGExporter(view.scene) - exporter.size = QtCore.QSize(200, 400) - exporter.margin = 5 - svg = exporter.render_to_svg() - - assert len(svg) == 1 - - element = svg[0] - assert element.tag == 'image' - assert element.get('width') == '30.0' - assert element.get('height') == '33.0' - assert element.get('transform') == 'rotate(0.0 -15.0 -20.0)' - assert element.get('x') == '5.0' - assert element.get('y') == '5.0' - - -def test_scene_to_svg_exporter_render_pixmap_with_rotation(view): - item = BeePixmapItem( - QtGui.QImage(100, 110, QtGui.QImage.Format.Format_RGB32)) - item.setPos(QtCore.QPointF(20, 30)) - item.setRotation(90) - view.scene.addItem(item) - exporter = SceneToSVGExporter(view.scene) - exporter.size = QtCore.QSize(200, 400) - exporter.margin = 5 - svg = exporter.render_to_svg() - - assert len(svg) == 1 - - element = svg[0] - assert element.tag == 'image' - assert element.get('transform') == 'rotate(90.0 115.0 5.0)' - assert element.get('x') == '115.0' - assert element.get('y') == '5.0' - - -def test_scene_to_svg_exporter_render_pixmap_with_opacity(view): - item = BeePixmapItem( - QtGui.QImage(100, 110, QtGui.QImage.Format.Format_RGB32)) - item.setPos(QtCore.QPointF(20, 30)) - item.setOpacity(0.75) - view.scene.addItem(item) - exporter = SceneToSVGExporter(view.scene) - exporter.size = QtCore.QSize(200, 400) - exporter.margin = 5 - svg = exporter.render_to_svg() - - assert len(svg) == 1 - - element = svg[0] - assert element.tag == 'image' - assert element.get('opacity') == '0.75' - - -def test_scene_to_svg_exporter_render_pixmap_with_flip(view): - item = BeePixmapItem( - QtGui.QImage(100, 110, QtGui.QImage.Format.Format_RGB32)) - item.setPos(QtCore.QPointF(20, 30)) - item.do_flip() - view.scene.addItem(item) - exporter = SceneToSVGExporter(view.scene) - exporter.size = QtCore.QSize(200, 400) - exporter.margin = 5 - svg = exporter.render_to_svg() - - assert len(svg) == 1 - - element = svg[0] - assert element.tag == 'image' - assert element.get('transform') == ( - 'translate(105.0 5.0) scale(-1.0 1)' - ' translate(-105.0 -5.0) rotate(0.0 105.0 5.0)') - assert element.get('x') == '105.0' - assert element.get('y') == '5.0' - - -def test_scene_to_svg_exporter_render_text(view): - item = BeeTextItem('foo') - item.setPos(QtCore.QPointF(20, 30)) - view.scene.addItem(item) - exporter = SceneToSVGExporter(view.scene) - exporter.size = QtCore.QSize(200, 400) - exporter.margin = 5 - svg = exporter.render_to_svg() - - assert len(svg) == 1 - - element = svg[0] - assert element.tag == 'text' - assert element.text == 'foo' - assert element.get('dominant-baseline') == 'hanging' - assert 'font-family' in element.get('style') - assert element.get('transform') == 'rotate(0.0 5.0 5.0)' - assert element.get('x') == '5.0' - assert element.get('y') == '5.0' - - -def test_scene_to_svg_exporter_export_when_file_not_writeable(view, tmpdir): - filename = os.path.join(tmpdir, 'foo.svg') - with open(filename, 'w') as f: - f.write('foo') - os.chmod(filename, stat.S_IREAD) - item = BeeTextItem('foo') - view.scene.addItem(item) - exporter = SceneToSVGExporter(view.scene) - exporter.size = QtCore.QSize(100, 120) - - with pytest.raises(BeeFileIOError) as e: - exporter.export(filename) - assert e.filename == filename - - -def test_scene_to_svg_exporter_render_with_worker(view): - item = BeeTextItem('foo') - item.setPos(QtCore.QPointF(20, 30)) - view.scene.addItem(item) - exporter = SceneToSVGExporter(view.scene) - exporter.size = QtCore.QSize(200, 400) - exporter.margin = 5 - - worker = MagicMock(canceled=False) - svg = exporter.render_to_svg(worker=worker) - assert len(svg) == 1 - worker.progress.emit.assert_called_once_with(0) - - -def test_scene_to_svg_exporter_render_with_worker_canceled(view): - item = BeeTextItem('foo') - item.setPos(QtCore.QPointF(20, 30)) - view.scene.addItem(item) - exporter = SceneToSVGExporter(view.scene) - exporter.size = QtCore.QSize(200, 400) - exporter.margin = 5 - - worker = MagicMock(canceled=True) - svg = exporter.render_to_svg(worker=worker) - assert svg is None - - -def test_scene_to_svg_exporter_export_writes_svg(view, tmpdir): - filename = os.path.join(tmpdir, 'foo.svg') - item = BeeTextItem('foo') - view.scene.addItem(item) - exporter = SceneToSVGExporter(view.scene) - exporter.size = QtCore.QSize(100, 120) - exporter.export(filename) - - with open(filename, 'rb') as f: - assert f.read().startswith(b'