diff --git a/CHANGELOG.rst b/CHANGELOG.rst index f2e6806..745aeec 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -8,6 +8,7 @@ Added settings. * The opacity of images can be changed (Images -> Change Opacity). * Images can be set to display as grayscale (Images -> Grayscale). +* The scene can now also be exported as SVG Fixed diff --git a/beeref/fileio/export.py b/beeref/fileio/export.py index 223483d..c5795c8 100644 --- a/beeref/fileio/export.py +++ b/beeref/fileio/export.py @@ -13,21 +13,40 @@ # You should have received a copy of the GNU General Public License # along with BeeRef. If not, see . +import base64 import logging +from xml.etree import ElementTree as ET from PyQt6 import QtCore, QtGui from .errors import BeeFileIOError -from beeref import constants +from beeref import constants, widgets logger = logging.getLogger(__name__) -class SceneToPixmapExporter: - """For exporting the scene to a single image.""" +class ExporterRegistry(dict): - MARGIN = 100 + DEFAULT_TYPE = 'default exporter' + + def __getitem__(self, key): + key = key.removeprefix('.') + exp = self.get(key, super().__getitem__(self.DEFAULT_TYPE)) + logger.debug(f'Exporter for type {key}: {exp}') + return exp + + +exporter_registry = ExporterRegistry() + + +def register_exporter(cls): + exporter_registry[cls.TYPE] = cls + return cls + + +class ExporterBase: + """For exporting the scene to a single image.""" def __init__(self, scene): self.scene = scene @@ -46,19 +65,40 @@ class SceneToPixmapExporter: logger.debug(f'Default export margin: {self.margin}') logger.debug(f'Default export size with margins: {self.default_size}') - def render_to_image(self, size): - logger.debug(f'Final export size: {size}') - margin = self.margin * size.width() / self.default_size.width() + +@register_exporter +class SceneToPixmapExporter(ExporterBase): + + TYPE = ExporterRegistry.DEFAULT_TYPE + + def get_user_input(self, parent): + """Ask user for final export size.""" + + dialog = widgets.SceneToPixmapExporterDialog( + parent=parent, + default_size=self.default_size, + ) + if dialog.exec(): + size = dialog.value() + logger.debug(f'Got export size {size}') + self.size = size + return True + else: + return False + + def render_to_image(self): + logger.debug(f'Final export size: {self.size}') + margin = self.margin * self.size.width() / self.default_size.width() logger.debug(f'Final export margin: {margin}') - image = QtGui.QImage(size, QtGui.QImage.Format.Format_RGB32) + image = QtGui.QImage(self.size, QtGui.QImage.Format.Format_RGB32) image.fill(QtGui.QColor(*constants.COLORS['Scene:Canvas'])) painter = QtGui.QPainter(image) target_rect = QtCore.QRectF( margin, margin, - size.width() - 2 * margin, - size.height() - 2 * margin) + self.size.width() - 2 * margin, + self.size.height() - 2 * margin) logger.trace(f'Final export target_rect: {target_rect}') self.scene.render(painter, source=self.scene.itemsBoundingRect(), @@ -66,9 +106,155 @@ class SceneToPixmapExporter: painter.end() return image - def export(self, filename, size): + def export(self, filename, worker=None): logger.debug(f'Exporting scene to {filename}') - image = self.render_to_image(size) + if worker: + worker.begin_processing.emit(1) + + image = self.render_to_image() + + if worker and worker.canceled: + logger.debug('Export canceled') + worker.finished.emit(filename, []) + return + if not image.save(filename, quality=90): - raise BeeFileIOError( - msg=str('Error writing image'), filename=filename) + msg = 'Error writing file' + logger.debug(f'Export failed: {msg}') + if worker: + worker.finished.emit(filename, [msg]) + return + else: + raise BeeFileIOError(msg, filename=filename) + + logger.debug('Export finished') + if worker: + worker.progress.emit(1) + worker.finished.emit(filename, []) + + +@register_exporter +class SceneToSVGExporter(ExporterBase): + + TYPE = 'svg' + + def get_user_input(self, parent): + self.size = self.default_size + return True + + def _get_textstyles(self, item): + fontstylemap = { + QtGui.QFont.Style.StyleNormal: 'normal', + QtGui.QFont.Style.StyleItalic: 'italic', + QtGui.QFont.Style.StyleOblique: 'oblique', + } + + font = item.font() + fontsize = font.pointSize() * item.scale() + families = ', '.join(font.families()) + fontstyle = fontstylemap[font.style()] + + return ('white-space:pre', + f'font-size:{fontsize}pt', + f'font-family:{families}', + f'font-weight:{font.weight()}', + f'font-stretch:{font.stretch()}', + f'font-style:{fontstyle}') + + def render_to_svg(self, worker=None): + svg = ET.Element( + 'svg', + attrib={'width': str(self.size.width()), + 'height': str(self.size.height()), + 'xmlns': 'http://www.w3.org/2000/svg', + 'xmlns:xlink': 'http://www.w3.org/1999/xlink', + }) + + rect = self.scene.itemsBoundingRect() + offset = rect.topLeft() - QtCore.QPointF(self.margin, self.margin) + + for i, item in enumerate(sorted(self.scene.items(), + key=lambda x: x.zValue())): + # z order in SVG specified via the order of elements in the tree + pos = item.pos() - offset + anchor = pos + + if item.TYPE == 'text': + styles = self._get_textstyles(item) + element = ET.Element( + 'text', + attrib={'style': ';'.join(styles), + 'dominant-baseline': 'hanging'}) + element.text = item.toPlainText() + if item.TYPE == 'pixmap': + width = item.width * item.scale() + height = item.height * item.scale() + pixmap, imgformat = item.pixmap_to_bytes( + apply_grayscale=True, + apply_crop=True) + pixmap = base64.b64encode(pixmap).decode('ascii') + element = ET.Element( + 'image', + attrib={ + 'xlink:href': + f'data:image/{imgformat};base64,{pixmap}', + 'width': str(width), + 'height': str(height), + 'image-rendering': ('crisp-edges' if item.scale() > 2 + else 'optimizeQuality')}) + pos = pos + item.crop.topLeft() + + transforms = [] + if item.flip() == -1: + # The following is not recognised by Inkscape and not an + # official standard: + # element.set('transform-origin', f'{anchor.x()} {anchor.y()}') + # Thus we need to fix the origin manually + transforms.append(f'translate({anchor.x()} {anchor.y()})') + transforms.append(f'scale({item.flip()} 1)') + transforms.append(f'translate(-{anchor.x()} -{anchor.y()})') + transforms.append( + f'rotate({item.rotation()} {anchor.x()} {anchor.y()})') + + element.set('transform', ' '.join(transforms)) + element.set('x', str(pos.x())) + element.set('y', str(pos.y())) + element.set('opacity', str(item.opacity())) + + svg.append(element) + if worker: + worker.progress.emit(i) + if 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())) + + svg = self.render_to_svg(worker) + + if worker and worker.canceled: + logger.debug('Export canceled') + worker.finished.emit(filename, []) + return + + tree = ET.ElementTree(svg) + ET.indent(tree, space=' ') + + try: + 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 + + logger.debug('Export finished') + if worker: + worker.finished.emit(filename, []) diff --git a/beeref/items.py b/beeref/items.py index 39d4aa8..1750021 100644 --- a/beeref/items.py +++ b/beeref/items.py @@ -169,12 +169,20 @@ class BeePixmapItem(BeeItemMixin, QtWidgets.QGraphicsPixmapItem): logger.debug(f'Found format {formt} for {self}') return formt - def pixmap_to_bytes(self): + def pixmap_to_bytes(self, apply_grayscale=False, apply_crop=False): """Convert the pixmap data to PNG bytestring.""" barray = QtCore.QByteArray() buffer = QtCore.QBuffer(barray) buffer.open(QtCore.QIODevice.OpenModeFlag.WriteOnly) - img = self.pixmap().toImage() + if apply_grayscale and self.grayscale: + pm = self._grayscale_pixmap + else: + pm = self.pixmap() + + if apply_crop: + pm = pm.copy(self.crop.toRect()) + + img = pm.toImage() imgformat = self.get_imgformat(img) img.save(buffer, imgformat.upper(), quality=90) return (barray.data(), imgformat) diff --git a/beeref/view.py b/beeref/view.py index fb92a0c..059d49d 100644 --- a/beeref/view.py +++ b/beeref/view.py @@ -26,7 +26,7 @@ from beeref import commands from beeref.config import CommandlineArgs, BeeSettings from beeref import constants from beeref import fileio -from beeref.fileio.export import SceneToPixmapExporter +from beeref.fileio.export import exporter_registry from beeref import widgets from beeref.items import BeePixmapItem, BeeTextItem from beeref.main_controls import MainControlsMixin @@ -416,31 +416,40 @@ class BeeGraphicsView(MainControlsMixin, parent=self, caption='Export Scene to Image', directory=directory, - filter=';;'.join(('Image Files (*.png *.jpg *.jpeg)', + filter=';;'.join(('Image Files (*.png *.jpg *.jpeg *.svg)', 'PNG (*.png)', - 'JPEG (*.jpg *.jpeg)'))) + 'JPEG (*.jpg *.jpeg)', + 'SVG (*.svg)'))) - if filename: - name, ext = os.path.splitext(filename) - if not ext: - ext = get_file_extension_from_format(formatstr) - filename = f'{filename}.{ext}' - logger.debug(f'Got export filename {filename}') - exporter = SceneToPixmapExporter(self.scene) - dialog = widgets.SceneToPixmapExporterDialog( - parent=self, - default_size=exporter.default_size, - ) - if dialog.exec(): - size = dialog.value() - logger.debug(f'Got export size {size}') - try: - exporter.export(filename, size) - except fileio.BeeFileIOError as e: - QtWidgets.QMessageBox.warning( - self, - 'Problem exporting scene', - str(e)) + if not filename: + return + + name, ext = os.path.splitext(filename) + if not ext: + ext = get_file_extension_from_format(formatstr) + filename = f'{filename}.{ext}' + logger.debug(f'Got export filename {filename}') + + exporter_cls = exporter_registry[ext] + exporter = exporter_cls(self.scene) + if not exporter.get_user_input(self): + return + + self.worker = fileio.ThreadedIO(exporter.export, filename) + self.worker.finished.connect(self.on_export_finished) + self.progress = widgets.BeeProgressDialog( + 'Exporting %s' % filename, + worker=self.worker, + parent=self) + self.worker.start() + + def on_export_finished(self, filename, errors): + if errors: + err_msg = '
'.join(str(errors)) + QtWidgets.QMessageBox.warning( + self, + 'Problem writing file', + f'

Problem writing file {filename}

{err_msg}

') def on_action_quit(self): logger.info('User quit. Exiting...') diff --git a/tests/fileio/test_export.py b/tests/fileio/test_export.py index 12b775e..5314cca 100644 --- a/tests/fileio/test_export.py +++ b/tests/fileio/test_export.py @@ -1,14 +1,45 @@ import os import stat -from unittest.mock import patch, ANY +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.items import BeePixmapItem, BeeTextItem from beeref.fileio.errors import BeeFileIOError -from beeref.fileio.export import SceneToPixmapExporter +from beeref.fileio.export import ( + exporter_registry, + SceneToPixmapExporter, + SceneToSVGExporter, +) + + +@pytest.mark.parametrize('key,expected', + [('png', SceneToPixmapExporter), + ('jpg', SceneToPixmapExporter), + ('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): @@ -54,7 +85,9 @@ def test_scene_to_pixmap_exporter_render_sets_margins(render_mock, view): exporter = SceneToPixmapExporter(view.scene) assert exporter.margin == 36 assert exporter.default_size == QtCore.QSize(1072, 1272) - exporter.render_to_image(QtCore.QSize(536, 636)) + + exporter.size = QtCore.QSize(536, 636) + exporter.render_to_image() render_mock.assert_called_once_with( ANY, @@ -70,24 +103,61 @@ def test_scene_to_pixmap_exporter_render_renders_scene(view): exporter = SceneToPixmapExporter(view.scene) assert exporter.margin == 36 assert exporter.default_size == QtCore.QSize(1072, 1272) - image = exporter.render_to_image(QtCore.QSize(536, 636)) + + 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_export_writes_image(view, tmpdir): +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.export(filename, QtCore.QSize(100, 120)) + 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_export_when_file_not_writeable(view, tmpdir): +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') @@ -96,7 +166,301 @@ def test_scene_to_pixmap_export_when_file_not_writeable(view, tmpdir): 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, QtCore.QSize(100, 120)) + 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'