Skip to content

Commit

Permalink
Add threading/progress bar to exports
Browse files Browse the repository at this point in the history
  • Loading branch information
rbreu committed Dec 30, 2023
1 parent cc47eb2 commit e43c4cd
Show file tree
Hide file tree
Showing 3 changed files with 89 additions and 29 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
91 changes: 67 additions & 24 deletions beeref/fileio/export.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,13 +106,29 @@ def render_to_image(self):
painter.end()
return image

def export(self, filename):
def export(self, filename, worker=None):
logger.debug(f'Exporting scene to {filename}')
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'
if worker:
worker.finished.emit(filename, [msg])
else:
raise BeeFileIOError(msg, filename=filename)

logger.debug('Export finished')
if worker:
worker.progress.emit(1)
worker.finished.emit(filename, [])


@register_exporter
Expand All @@ -124,7 +140,26 @@ def get_user_input(self, parent):
self.size = self.default_size
return True

def render_to_svg(self):
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):
svg = ET.Element(
'svg',
attrib={'width': str(self.size.width()),
Expand All @@ -136,20 +171,16 @@ def render_to_svg(self):
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()):
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':
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}')
styles = self._get_textstyles(item)
element.set('style', ';'.join(styles))
element.set('dominant-baseline', 'hanging')
if item.TYPE == 'pixmap':
Expand All @@ -161,43 +192,51 @@ def render_to_svg(self):
'image',
attrib={'xlink:href':
f'data:image/{imgformat};base64,{pixmap}'})
width = item.crop.width() * item.scale()
height = item.crop.height() * item.scale()
width = item.width * item.scale()
height = item.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:
# 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('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):
def export(self, filename, worker=None):
logger.debug(f'Exporting scene to {filename}')
svg = self.render_to_svg()
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=' ')
Expand All @@ -206,7 +245,11 @@ def export(self, filename):
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)
if worker:
worker.finished.emit(filename, [str(e)])
else:
raise BeeFileIOError(msg=str(e), filename=filename) from e

logger.debug('Export finished')
if worker:
worker.finished.emit(filename, [])
26 changes: 21 additions & 5 deletions beeref/view.py
Original file line number Diff line number Diff line change
Expand Up @@ -435,13 +435,29 @@ def on_action_export_scene(self):
if not exporter.get_user_input(self):
return

try:
exporter.export(filename)
except fileio.BeeFileIOError as e:
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()

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

def on_export_finished(self, filename, errors):
if errors:
err_msg = '</br>'.join(str(errors))
QtWidgets.QMessageBox.warning(
self,
'Problem exporting scene',
str(e))
'Problem writing file',
f'<p>Problem writing file {filename}</p><p>{err_msg}</p>')

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

0 comments on commit e43c4cd

Please sign in to comment.