Add export format SVG

This commit is contained in:
Rebecca Breu 2023-12-29 16:59:42 +01:00
parent b0775cace4
commit 1eab4712d3
7 changed files with 686 additions and 51 deletions

View file

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

View file

@ -13,21 +13,40 @@
# You should have received a copy of the GNU General Public License
# along with BeeRef. If not, see <https://www.gnu.org/licenses/>.
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, [])

View file

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

View file

@ -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 = '</br>'.join(str(errors))
QtWidgets.QMessageBox.warning(
self,
'Problem writing file',
f'<p>Problem writing file {filename}</p><p>{err_msg}</p>')
def on_action_quit(self):
logger.info('User quit. Exiting...')

View file

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

@ -164,17 +164,73 @@ def test_get_imgformat_png_when_setting_png(
def test_pixmap_to_bytes_png(qapp, imgfilename3x3):
item = BeePixmapItem(QtGui.QImage(imgfilename3x3))
item.crop = QtCore.QRectF(0, 0, 2, 2)
item.grayscale = True
data, imgformat = item.pixmap_to_bytes()
assert imgformat == 'png'
assert data.startswith(b'\x89PNG')
pixmap = QtGui.QPixmap()
pixmap.loadFromData(data)
img = pixmap.toImage()
assert img.allGray() is False
assert img.size() == QtCore.QSize(3, 3)
def test_pixmap_to_bytes_jpg(qapp, imgfilename3x3, settings):
settings.setValue('Items/image_storage_format', 'jpg')
item = BeePixmapItem(QtGui.QImage(imgfilename3x3))
item.crop = QtCore.QRectF(0, 0, 2, 2)
item.grayscale = True
data, imgformat = item.pixmap_to_bytes()
assert imgformat == 'jpg'
assert data.startswith(b'\xff\xd8\xff\xe0\x00\x10JFIF')
pixmap = QtGui.QPixmap()
pixmap.loadFromData(data)
img = pixmap.toImage()
assert img.allGray() is False
assert img.size() == QtCore.QSize(3, 3)
def test_pixmap_to_bytes_apply_grayscale(qapp, imgfilename3x3):
item = BeePixmapItem(QtGui.QImage(imgfilename3x3))
item.crop = QtCore.QRectF(0, 0, 2, 2)
item.grayscale = True
data, imgformat = item.pixmap_to_bytes(apply_grayscale=True)
assert imgformat == 'png'
assert data.startswith(b'\x89PNG')
pixmap = QtGui.QPixmap()
pixmap.loadFromData(data)
img = pixmap.toImage()
assert img.allGray() is True
assert img.size() == QtCore.QSize(3, 3)
def test_pixmap_to_bytes_apply_crop(qapp, imgfilename3x3):
item = BeePixmapItem(QtGui.QImage(imgfilename3x3))
item.crop = QtCore.QRectF(0, 0, 2, 2)
item.grayscale = True
data, imgformat = item.pixmap_to_bytes(apply_crop=True)
assert imgformat == 'png'
assert data.startswith(b'\x89PNG')
pixmap = QtGui.QPixmap()
pixmap.loadFromData(data)
img = pixmap.toImage()
assert img.allGray() is False
assert img.size() == QtCore.QSize(2, 2)
def test_pixmap_to_bytes_apply_grayscale_crop_when_not_set(
qapp, imgfilename3x3):
item = BeePixmapItem(QtGui.QImage(imgfilename3x3))
data, imgformat = item.pixmap_to_bytes(apply_grayscale=True,
apply_crop=False)
assert imgformat == 'png'
assert data.startswith(b'\x89PNG')
pixmap = QtGui.QPixmap()
pixmap.loadFromData(data)
img = pixmap.toImage()
assert img.allGray() is False
assert img.size() == QtCore.QSize(3, 3)
def test_pixmap_from_bytes(qapp, item, imgfilename3x3):

View file

@ -298,7 +298,7 @@ def test_on_action_save_when_no_filename(save_as_mock, view, imgfilename3x3):
@patch('beeref.widgets.SceneToPixmapExporterDialog.value')
@patch('PyQt6.QtWidgets.QFileDialog.getSaveFileName')
def test_on_action_export_scene(
file_mock, value_mock, exec_mock, view, tmpdir):
file_mock, value_mock, exec_mock, view, tmpdir, qtbot):
item = BeeTextItem('foo')
view.scene.addItem(item)
filename = os.path.join(tmpdir, 'test.png')
@ -306,7 +306,11 @@ def test_on_action_export_scene(
file_mock.return_value = (filename, None)
exec_mock.return_value = 1
value_mock.return_value = QtCore.QSize(100, 100)
view.on_export_finished = MagicMock()
view.on_action_export_scene()
qtbot.waitUntil(lambda: view.on_export_finished.called is True)
view.on_export_finished.assert_called_once_with(filename, [])
img = QtGui.QImage(filename)
assert img.size() == QtCore.QSize(100, 100)
@ -315,7 +319,7 @@ def test_on_action_export_scene(
@patch('beeref.widgets.SceneToPixmapExporterDialog.value')
@patch('PyQt6.QtWidgets.QFileDialog.getSaveFileName')
def test_on_action_export_scene_no_file_extension(
file_mock, value_mock, exec_mock, view, tmpdir):
file_mock, value_mock, exec_mock, view, tmpdir, qtbot):
item = BeeTextItem('foo')
view.scene.addItem(item)
filename = os.path.join(tmpdir, 'test')
@ -323,7 +327,11 @@ def test_on_action_export_scene_no_file_extension(
file_mock.return_value = (filename, 'PNG (*.png)')
exec_mock.return_value = 1
value_mock.return_value = QtCore.QSize(100, 100)
view.on_export_finished = MagicMock()
view.on_action_export_scene()
qtbot.waitUntil(lambda: view.on_export_finished.called is True)
view.on_export_finished.assert_called_once_with(f'{filename}.png', [])
img = QtGui.QImage(f'{filename}.png')
assert img.size() == QtCore.QSize(100, 100)
@ -336,9 +344,12 @@ def test_on_action_export_scene_no_filename(
item = BeeTextItem('foo')
view.scene.addItem(item)
file_mock.return_value = (None, None)
view.on_export_finished = MagicMock()
view.on_action_export_scene()
exec_mock.assert_not_called()
value_mock.assert_not_called()
view.on_export_finished.assert_not_called()
@patch('beeref.widgets.SceneToPixmapExporterDialog.exec')