Skip to content

Commit

Permalink
Add export format SVG
Browse files Browse the repository at this point in the history
  • Loading branch information
rbreu committed Dec 29, 2023
1 parent b0775ca commit cc47eb2
Show file tree
Hide file tree
Showing 3 changed files with 185 additions and 38 deletions.
162 changes: 150 additions & 12 deletions beeref/fileio/export.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):

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

MARGIN = 100

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
Expand All @@ -46,29 +65,148 @@ def __init__(self, scene):
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(),
target=target_rect)
painter.end()
return image

def export(self, filename, size):
def export(self, filename):
logger.debug(f'Exporting scene to {filename}')
image = self.render_to_image(size)
image = self.render_to_image()
if not image.save(filename, quality=90):
raise BeeFileIOError(
msg=str('Error writing image'), filename=filename)
logger.debug('Export finished')


@register_exporter
class SceneToSVGExporter(ExporterBase):

TYPE = 'svg'

def get_user_input(self, parent):
self.size = self.default_size
return True

def render_to_svg(self):
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 item in 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':
element = ET.Element('text')
element.text = item.toPlainText()
styles = ['white-space:pre']
font = item.font()
fontsize = font.pointSize() * item.scale()
styles.append(f'font-size:{fontsize}pt')
families = ', '.join(font.families())
styles.append(f'font-family:{families}')
element.set('style', ';'.join(styles))
element.set('dominant-baseline', 'hanging')
if item.TYPE == 'pixmap':
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 = item.crop.width() * item.scale()
height = item.crop.height() * item.scale()
element.set('width', str(width))
element.set('height', str(height))
print(item.scale())
element.set(
'image-rendering',
'crisp-edges' if item.scale() > 2 else 'optimizeQuality')
pos = pos + item.crop.topLeft()

element.set('opacity', str(item.opacity()))

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

svg.append(element)

return svg

def export(self, filename):
logger.debug(f'Exporting scene to {filename}')
svg = self.render_to_svg()

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:
raise BeeFileIOError(
msg=str(f'Error writing image: {e}'), filename=filename)

logger.debug('Export finished')
12 changes: 10 additions & 2 deletions beeref/items.py
Original file line number Diff line number Diff line change
Expand Up @@ -169,12 +169,20 @@ def get_imgformat(self, img):
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)
Expand Down
49 changes: 25 additions & 24 deletions beeref/view.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
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
Expand Down Expand Up @@ -416,31 +416,32 @@ def on_action_export_scene(self):
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

try:
exporter.export(filename)
except fileio.BeeFileIOError as e:
QtWidgets.QMessageBox.warning(
self,
'Problem exporting scene',
str(e))

def on_action_quit(self):
logger.info('User quit. Exiting...')
Expand Down

0 comments on commit cc47eb2

Please sign in to comment.