Export all images from scene

This commit is contained in:
Rebecca Breu 2024-05-19 19:05:31 +02:00
parent 38aaaf2553
commit 479665b9f1
16 changed files with 1122 additions and 494 deletions

View file

@ -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

View file

@ -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',

View file

@ -29,6 +29,7 @@ menu_structure = [
'save',
'save_as',
'export_scene',
'export_images',
MENU_SEPARATOR,
'quit',
],

View file

@ -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__()

View file

@ -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, [])

View file

@ -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 (?, ?, ?, ?, ?)',

View file

@ -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."""

View file

@ -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'<p>Problem writing file {filename}</p><p>{err_msg}</p>')
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()

View file

@ -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

View file

@ -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'<?xml')
def test_scene_to_svg_exporter_export_with_worker(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)
worker = MagicMock(canceled=False)
exporter.export(filename, worker)
worker.begin_processing.emit.assert_called_once_with(1)
worker.progress.emit.assert_called_once_with(0)
worker.finished.emit.assert_called_once_with(filename, [])
with open(filename, 'rb') as f:
assert f.read().startswith(b'<?xml')
def test_scene_to_svg_exporter_export_with_worker_canceled(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)
worker = MagicMock(canceled=True)
exporter.export(filename, worker)
worker.begin_processing.emit.assert_called_once_with(1)
worker.progress.emit.assert_called_once_with(0)
worker.finished.emit.assert_called_once_with(filename, [])
os.path.exists(filename) is False
def test_scene_to_svg_exporter_export_when_file_not_writeable_with_worker(
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)
worker = MagicMock(canceled=False)
exporter.export(filename, worker=worker)
worker.begin_processing.emit.assert_called_once_with(1)
worker.progress.emit.assert_called_once_with(0)
worker.finished.emit.assert_called_once()
args = worker.finished.emit.call_args.args
assert args[0] == filename
assert len(args[1]) == 1

View file

@ -0,0 +1,287 @@
import os
import stat
from unittest.mock import MagicMock
import pytest
from PyQt6 import QtGui
from beeref.items import BeePixmapItem
from beeref.fileio.errors import BeeFileIOError
from beeref.fileio.export import ImagesToDirectoryExporter
def test_images_to_directory_exporter_export_writes_images(
view, tmpdir, imgdata3x3, imgfilename3x3,):
item1 = BeePixmapItem(QtGui.QImage(imgfilename3x3))
view.scene.addItem(item1)
item2 = BeePixmapItem(QtGui.QImage(imgfilename3x3))
item2.save_id = 3
view.scene.addItem(item2)
exporter = ImagesToDirectoryExporter(view.scene, tmpdir)
exporter.export()
with open(os.path.join(tmpdir, '0003.png'), 'rb') as f:
assert f.read().startswith(b'\x89PNG')
with open(os.path.join(tmpdir, '0004.png'), 'rb') as f:
assert f.read().startswith(b'\x89PNG')
def test_images_to_directory_exporter_export_file_exists_no_user_input(
view, tmpdir, imgdata3x3, imgfilename3x3,):
item1 = BeePixmapItem(QtGui.QImage(imgfilename3x3))
view.scene.addItem(item1)
item2 = BeePixmapItem(QtGui.QImage(imgfilename3x3))
view.scene.addItem(item2)
with open(os.path.join(tmpdir, '0002.png'), 'w') as f:
assert f.write('foo')
exporter = ImagesToDirectoryExporter(view.scene, tmpdir)
exporter.export()
with open(os.path.join(tmpdir, '0001.png'), 'rb') as f:
assert f.read().startswith(b'\x89PNG')
with open(os.path.join(tmpdir, '0002.png'), 'r') as f:
assert f.read() == 'foo'
assert exporter.start_from == 1
def test_images_to_directory_exporter_export_file_exists_skip(
view, tmpdir, imgdata3x3, imgfilename3x3,):
item1 = BeePixmapItem(QtGui.QImage(imgfilename3x3))
view.scene.addItem(item1)
item2 = BeePixmapItem(QtGui.QImage(imgfilename3x3))
view.scene.addItem(item2)
item3 = BeePixmapItem(QtGui.QImage(imgfilename3x3))
view.scene.addItem(item3)
with open(os.path.join(tmpdir, '0002.png'), 'w') as f:
assert f.write('foo')
with open(os.path.join(tmpdir, '0003.png'), 'w') as f:
assert f.write('bar')
exporter = ImagesToDirectoryExporter(view.scene, tmpdir)
exporter.handle_existing = 'skip'
exporter.export()
with open(os.path.join(tmpdir, '0001.png'), 'rb') as f:
assert f.read().startswith(b'\x89PNG')
with open(os.path.join(tmpdir, '0002.png'), 'r') as f:
assert f.read() == 'foo'
with open(os.path.join(tmpdir, '0003.png'), 'r') as f:
assert f.read() == 'bar'
assert exporter.start_from == 2
assert exporter.handle_existing is None
def test_images_to_directory_exporter_export_file_exists_skip_all(
view, tmpdir, imgdata3x3, imgfilename3x3,):
item1 = BeePixmapItem(QtGui.QImage(imgfilename3x3))
view.scene.addItem(item1)
item2 = BeePixmapItem(QtGui.QImage(imgfilename3x3))
view.scene.addItem(item2)
item3 = BeePixmapItem(QtGui.QImage(imgfilename3x3))
view.scene.addItem(item3)
with open(os.path.join(tmpdir, '0002.png'), 'w') as f:
assert f.write('foo')
with open(os.path.join(tmpdir, '0003.png'), 'w') as f:
assert f.write('bar')
exporter = ImagesToDirectoryExporter(view.scene, tmpdir)
exporter.handle_existing = 'skip_all'
exporter.export()
with open(os.path.join(tmpdir, '0001.png'), 'rb') as f:
assert f.read().startswith(b'\x89PNG')
with open(os.path.join(tmpdir, '0002.png'), 'r') as f:
assert f.read() == 'foo'
with open(os.path.join(tmpdir, '0003.png'), 'r') as f:
assert f.read() == 'bar'
assert exporter.handle_existing == 'skip_all'
def test_images_to_directory_exporter_export_file_exists_overwrite(
view, tmpdir, imgdata3x3, imgfilename3x3,):
item1 = BeePixmapItem(QtGui.QImage(imgfilename3x3))
view.scene.addItem(item1)
item2 = BeePixmapItem(QtGui.QImage(imgfilename3x3))
view.scene.addItem(item2)
item3 = BeePixmapItem(QtGui.QImage(imgfilename3x3))
view.scene.addItem(item3)
with open(os.path.join(tmpdir, '0002.png'), 'w') as f:
assert f.write('foo')
with open(os.path.join(tmpdir, '0003.png'), 'w') as f:
assert f.write('bar')
exporter = ImagesToDirectoryExporter(view.scene, tmpdir)
exporter.handle_existing = 'overwrite'
exporter.export()
with open(os.path.join(tmpdir, '0001.png'), 'rb') as f:
assert f.read().startswith(b'\x89PNG')
with open(os.path.join(tmpdir, '0002.png'), 'rb') as f:
assert f.read().startswith(b'\x89PNG')
with open(os.path.join(tmpdir, '0003.png'), 'r') as f:
assert f.read() == 'bar'
assert exporter.start_from == 2
assert exporter.handle_existing is None
def test_images_to_directory_exporter_export_file_exists_overwrite_all(
view, tmpdir, imgdata3x3, imgfilename3x3,):
item1 = BeePixmapItem(QtGui.QImage(imgfilename3x3))
view.scene.addItem(item1)
item2 = BeePixmapItem(QtGui.QImage(imgfilename3x3))
view.scene.addItem(item2)
item3 = BeePixmapItem(QtGui.QImage(imgfilename3x3))
view.scene.addItem(item3)
with open(os.path.join(tmpdir, '0002.png'), 'w') as f:
assert f.write('foo')
with open(os.path.join(tmpdir, '0003.png'), 'w') as f:
assert f.write('bar')
exporter = ImagesToDirectoryExporter(view.scene, tmpdir)
exporter.handle_existing = 'overwrite_all'
exporter.export()
with open(os.path.join(tmpdir, '0001.png'), 'rb') as f:
assert f.read().startswith(b'\x89PNG')
with open(os.path.join(tmpdir, '0002.png'), 'rb') as f:
assert f.read().startswith(b'\x89PNG')
with open(os.path.join(tmpdir, '0003.png'), 'rb') as f:
assert f.read().startswith(b'\x89PNG')
assert exporter.handle_existing == 'overwrite_all'
def test_images_to_directory_exporter_export_with_worker(
view, tmpdir, imgdata3x3, imgfilename3x3,):
item = BeePixmapItem(QtGui.QImage(imgfilename3x3))
view.scene.addItem(item)
worker = MagicMock(canceled=False)
exporter = ImagesToDirectoryExporter(view.scene, tmpdir)
exporter.export(worker)
with open(os.path.join(tmpdir, '0001.png'), 'rb') as f:
assert f.read().startswith(b'\x89PNG')
worker.begin_processing.emit.assert_called_once_with(1)
worker.progress.emit.assert_called_with(0)
worker.finished.emit.assert_called_once_with(tmpdir, [])
def test_images_to_directory_exporter_export_with_worker_when_canceled(
view, tmpdir, imgdata3x3, imgfilename3x3,):
item = BeePixmapItem(QtGui.QImage(imgfilename3x3))
view.scene.addItem(item)
worker = MagicMock(canceled=True)
exporter = ImagesToDirectoryExporter(view.scene, tmpdir)
exporter.export(worker)
assert os.path.exists(os.path.join(tmpdir, '0001.png')) is False
worker.begin_processing.emit.assert_called_once_with(1)
worker.progress.emit.assert_called_once_with(0)
worker.finished.emit.assert_called_once_with(tmpdir, [])
def test_images_to_directory_exporter_export_with_worker_when_file_exists(
view, tmpdir, imgdata3x3, imgfilename3x3,):
item = BeePixmapItem(QtGui.QImage(imgfilename3x3))
view.scene.addItem(item)
with open(os.path.join(tmpdir, '0001.png'), 'w') as f:
assert f.write('foo')
worker = MagicMock(canceled=False)
exporter = ImagesToDirectoryExporter(view.scene, tmpdir)
exporter.export(worker)
imgfilename = os.path.join(tmpdir, '0001.png')
with open(imgfilename, 'r') as f:
assert f.read() == 'foo'
worker.begin_processing.emit.assert_called_once_with(1)
worker.progress.emit.assert_called_with(0)
worker.user_input_required.emit.assert_called_once_with(imgfilename)
def test_images_to_directory_exporter_export_when_dir_not_writeable(
view, tmpdir, imgdata3x3, imgfilename3x3,):
item = BeePixmapItem(QtGui.QImage(imgfilename3x3))
view.scene.addItem(item)
os.chmod(tmpdir, stat.S_IREAD)
exporter = ImagesToDirectoryExporter(view.scene, tmpdir)
with pytest.raises(BeeFileIOError) as e:
exporter.export()
assert e.filename == tmpdir
def test_images_to_directory_exporter_export_when_dir_not_writeable_w_worker(
view, tmpdir, imgdata3x3, imgfilename3x3,):
item = BeePixmapItem(QtGui.QImage(imgfilename3x3))
view.scene.addItem(item)
os.chmod(tmpdir, stat.S_IREAD)
exporter = ImagesToDirectoryExporter(view.scene, tmpdir)
worker = MagicMock(canceled=False)
exporter.export(worker)
worker.begin_processing.emit.assert_called_once_with(1)
worker.finished.emit.assert_called_once()
args = worker.finished.emit.call_args.args
assert args[0] == tmpdir
assert len(args[1]) == 1
def test_images_to_directory_exporter_export_when_img_not_writeable(
view, tmpdir, imgdata3x3, imgfilename3x3,):
item = BeePixmapItem(QtGui.QImage(imgfilename3x3))
view.scene.addItem(item)
imgfilename = os.path.join(tmpdir, '0001.png')
with open(imgfilename, 'w') as f:
assert f.write('foo')
os.chmod(imgfilename, stat.S_IREAD)
exporter = ImagesToDirectoryExporter(view.scene, tmpdir)
exporter.handle_existing = 'overwrite_all'
with pytest.raises(BeeFileIOError) as e:
exporter.export()
assert e.filename == tmpdir
def test_images_to_directory_exporter_export_when_img_not_writeable_w_worker(
view, tmpdir, imgdata3x3, imgfilename3x3,):
item = BeePixmapItem(QtGui.QImage(imgfilename3x3))
view.scene.addItem(item)
imgfilename = os.path.join(tmpdir, '0001.png')
with open(imgfilename, 'w') as f:
assert f.write('foo')
os.chmod(imgfilename, stat.S_IREAD)
exporter = ImagesToDirectoryExporter(view.scene, tmpdir)
exporter.handle_existing = 'overwrite_all'
worker = MagicMock(canceled=False)
exporter.export(worker)
worker.begin_processing.emit.assert_called_once_with(1)
worker.finished.emit.assert_called_once()
args = worker.finished.emit.call_args.args
assert args[0] == imgfilename
assert len(args[1]) == 1

View file

@ -0,0 +1,181 @@
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
from beeref.fileio.errors import BeeFileIOError
from beeref.fileio.export import SceneToPixmapExporter
@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'])

View file

@ -0,0 +1,283 @@
import os
import stat
from unittest.mock import MagicMock
import pytest
from PyQt6 import QtGui, QtCore
from beeref.items import BeePixmapItem, BeeTextItem
from beeref.fileio.errors import BeeFileIOError
from beeref.fileio.export import SceneToSVGExporter
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'<?xml')
def test_scene_to_svg_exporter_export_with_worker(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)
worker = MagicMock(canceled=False)
exporter.export(filename, worker)
worker.begin_processing.emit.assert_called_once_with(1)
worker.progress.emit.assert_called_once_with(0)
worker.finished.emit.assert_called_once_with(filename, [])
with open(filename, 'rb') as f:
assert f.read().startswith(b'<?xml')
def test_scene_to_svg_exporter_export_with_worker_canceled(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)
worker = MagicMock(canceled=True)
exporter.export(filename, worker)
worker.begin_processing.emit.assert_called_once_with(1)
worker.progress.emit.assert_called_once_with(0)
worker.finished.emit.assert_called_once_with(filename, [])
os.path.exists(filename) is False
def test_scene_to_svg_exporter_export_when_file_not_writeable_with_worker(
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)
worker = MagicMock(canceled=False)
exporter.export(filename, worker=worker)
worker.begin_processing.emit.assert_called_once_with(1)
worker.progress.emit.assert_called_once_with(0)
worker.finished.emit.assert_called_once()
args = worker.finished.emit.call_args.args
assert args[0] == filename
assert len(args[1]) == 1

View file

@ -98,6 +98,43 @@ def test_get_extra_save_data(item):
}
def test_get_filename_for_export_when_save_id_and_filename(item):
item.filename = 'foo.png'
item.save_id = 5
assert item.get_filename_for_export('jpg', 8) == '0005-foo.jpg'
def test_get_filename_for_export_when_save_id_and_no_filename(item):
item.filename = None
item.save_id = 5
assert item.get_filename_for_export('jpg', 8) == '0005.jpg'
def test_get_filename_for_export_when_no_save_id_and_filename(item):
item.filename = 'foo.png'
item.save_id = None
assert item.get_filename_for_export('jpg', 8) == '0008-foo.jpg'
def test_get_filename_for_export_when_no_save_id_and_no_filename(item):
item.filename = None
item.save_id = None
assert item.get_filename_for_export('jpg', 8) == '0008.jpg'
def test_get_filename_for_export_when_save_id_and_no_default(item):
item.filename = 'foo.png'
item.save_id = 5
assert item.get_filename_for_export('jpg') == '0005-foo.jpg'
def test_get_filename_for_export_when_no_save_id_and_no_default(item):
item.filename = 'foo.png'
item.save_id = None
with pytest.raises(AssertionError):
assert item.get_filename_for_export('jpg')
def test_get_imgformat_test_with_real_image(
qapp, imgfilename3x3, item, settings):
settings.setValue('Items/image_storage_format', 'best')

View file

@ -1,4 +1,5 @@
import os.path
from pathlib import Path
import shutil
import sqlite3
from unittest.mock import MagicMock, patch, mock_open
@ -370,6 +371,99 @@ def test_on_action_export_scene_settings_input_canceled(
assert os.path.exists(filename) is False
@patch('PyQt6.QtWidgets.QFileDialog.getExistingDirectory')
def test_on_action_export_images(
dir_mock, view, tmpdir, qtbot, imgfilename3x3):
item = BeePixmapItem(QtGui.QImage(imgfilename3x3))
view.scene.addItem(item)
dir_mock.return_value = tmpdir
view.on_export_finished = MagicMock()
view.on_action_export_images()
qtbot.waitUntil(lambda: view.on_export_finished.called is True)
view.on_export_finished.assert_called_once_with(tmpdir, [])
assert os.path.exists(os.path.join(tmpdir, '0001.png'))
@patch('PyQt6.QtWidgets.QFileDialog.getExistingDirectory')
def test_on_action_export_images_no_dirname(
dir_mock, view, tmpdir, qtbot, imgfilename3x3):
item = BeePixmapItem(QtGui.QImage(imgfilename3x3))
view.scene.addItem(item)
dir_mock.return_value = None
view.on_export_finished = MagicMock()
view.on_action_export_images()
view.on_export_finished.assert_not_called()
assert os.path.exists(os.path.join(tmpdir, '0001.png')) is False
@patch('beeref.widgets.ExportImagesFileExistsDialog.exec',
return_value=QtWidgets.QDialog.DialogCode.Accepted)
@patch('beeref.widgets.ExportImagesFileExistsDialog.get_answer',
return_value='overwrite')
@patch('PyQt6.QtWidgets.QFileDialog.getExistingDirectory')
def test_on_action_export_images_file_exists_overwrite(
dir_mock, answer_mock, exec_mock, view, tmpdir, qtbot, imgfilename3x3):
item = BeePixmapItem(QtGui.QImage(imgfilename3x3))
view.scene.addItem(item)
dir_mock.return_value = tmpdir
view.on_export_finished = MagicMock()
imgfilename = Path(tmpdir) / '0001.png'
imgfilename.write_text('foo')
view.on_action_export_images()
qtbot.waitUntil(lambda: view.on_export_finished.called is True)
view.on_export_finished.assert_called_once_with(tmpdir, [])
answer_mock.assert_called_once_with()
exec_mock.assert_called_once_with()
imgfilename.read_bytes().startswith(b'\x89PNG')
@patch('beeref.widgets.ExportImagesFileExistsDialog.exec',
return_value=QtWidgets.QDialog.DialogCode.Accepted)
@patch('beeref.widgets.ExportImagesFileExistsDialog.get_answer',
return_value='skip')
@patch('PyQt6.QtWidgets.QFileDialog.getExistingDirectory')
def test_on_action_export_images_file_exists_skip(
dir_mock, answer_mock, exec_mock, view, tmpdir, qtbot, imgfilename3x3):
item = BeePixmapItem(QtGui.QImage(imgfilename3x3))
view.scene.addItem(item)
dir_mock.return_value = tmpdir
view.on_export_finished = MagicMock()
imgfilename = Path(tmpdir) / '0001.png'
imgfilename.write_text('foo')
view.on_action_export_images()
qtbot.waitUntil(lambda: view.on_export_finished.called is True)
view.on_export_finished.assert_called_once_with(tmpdir, [])
answer_mock.assert_called_once_with()
exec_mock.assert_called_once_with()
imgfilename.read_text() == 'foo'
@patch('beeref.widgets.ExportImagesFileExistsDialog.exec',
return_value=QtWidgets.QDialog.DialogCode.Rejected)
@patch('beeref.widgets.ExportImagesFileExistsDialog.get_answer',
return_value='skip')
@patch('PyQt6.QtWidgets.QFileDialog.getExistingDirectory')
def test_on_action_export_images_file_exists_canceled(
dir_mock, answer_mock, exec_mock, view, tmpdir, qtbot, imgfilename3x3):
item = BeePixmapItem(QtGui.QImage(imgfilename3x3))
view.scene.addItem(item)
dir_mock.return_value = tmpdir
view.on_export_finished = MagicMock()
imgfilename = Path(tmpdir) / '0001.png'
imgfilename.write_text('foo')
view.on_action_export_images()
qtbot.waitUntil(lambda: exec_mock.called is True)
view.on_export_finished.assert_not_called()
answer_mock.assert_not_called()
imgfilename.read_text() == 'foo'
@patch('beeref.widgets.settings.SettingsDialog.show')
def test_on_action_settings(show_mock, view):
view.on_action_settings()

View file

@ -8,6 +8,7 @@ from beeref.widgets import (
BeeNotification,
ChangeOpacityDialog,
DebugLogDialog,
ExportImagesFileExistsDialog,
SampleColorWidget,
SceneToPixmapExporterDialog,
)
@ -139,3 +140,15 @@ def test_sample_color_widget_paint_event_when_no_color(view):
brush = QtGui.QBrush(QtGui.QColor(0, 0, 0, 0))
painter_mock.setBrush.assert_called_once_with(brush)
painter_mock.drawRect.assert_called_once_with(0, 0, 50, 50)
def test_export_images_file_exists_dialog(view):
dlg = ExportImagesFileExistsDialog(view, '/tmp/foo.png')
assert len(dlg.radio_buttons) == 4
assert dlg.get_answer() == 'skip'
def test_export_images_file_exists_dialog_get_answer(view):
dlg = ExportImagesFileExistsDialog(view, '/tmp/foo.png')
dlg.radio_buttons['overwrite'].setChecked(True)
assert dlg.get_answer() == 'overwrite'