From 34a16e145eb084af1ba86d3df70984f92956a725 Mon Sep 17 00:00:00 2001 From: janezd Date: Fri, 21 Apr 2023 13:22:24 +0200 Subject: [PATCH] Add dpi to saved PNGs, and scale images in reports --- orangewidget/io.py | 69 ++++++++++++++++++++++++++--------- orangewidget/report/report.py | 18 ++++++--- 2 files changed, 63 insertions(+), 24 deletions(-) diff --git a/orangewidget/io.py b/orangewidget/io.py index 0b85f3cdf..dfbdcdc02 100644 --- a/orangewidget/io.py +++ b/orangewidget/io.py @@ -96,6 +96,25 @@ def _setup_painter(cls, painter, object, source_rect, buffer): def _save_buffer(buffer, filename): raise NotImplementedError + @staticmethod + def _meta_data(buffer): + meta_data = {} + + try: + size = buffer.size() + except AttributeError: + pass + else: + meta_data["width"] = size.width() + meta_data["height"] = size.height() + + try: + meta_data["pixel_ratio"] = buffer.devicePixelRatio() + except AttributeError: + pass + + return meta_data + @staticmethod def _get_exporter(): raise NotImplementedError @@ -130,8 +149,7 @@ def save_pyqtgraph(): exporter = cls._get_exporter() scene = object.scene() if scene is None: - cls._export(exporter(scene), filename) - return + return cls._export(exporter(scene), filename) views = scene.views() if views: # preserve scene rect and background brush @@ -141,13 +159,13 @@ def save_pyqtgraph(): view = scene.views()[0] scene.setSceneRect(view.sceneRect()) scene.setBackgroundBrush(effective_background(scene, view)) - cls._export(exporter(scene), filename) + return cls._export(exporter(scene), filename) finally: # reset scene rect and background brush scene.setBackgroundBrush(backgroundbrush) scene.setSceneRect(scenerect) else: - cls._export(exporter(scene), filename) + return cls._export(exporter(scene), filename) def save_scene(): assert isinstance(object, QGraphicsScene) @@ -155,8 +173,7 @@ def save_scene(): views = object.views() if not views: rect = object.itemsBoundingRect() - _render(rect, ratio, rect.size(), object) - return + return _render(rect, ratio, rect.size(), object) # Pick the first view. If there's a widget with multiple views that # cares which one is used, it must set graph_name to view, not scene @@ -166,11 +183,11 @@ def save_scene(): source_rect = QRect( int(target_rect.x()), int(target_rect.y()), int(target_rect.width()), int(target_rect.height())) - _render(source_rect, ratio, target_rect.size(), view) + return _render(source_rect, ratio, target_rect.size(), view) def save_widget(): assert isinstance(object, QWidget) - _render(object.rect(), object.devicePixelRatio(), object.size(), + return _render(object.rect(), object.devicePixelRatio(), object.size(), object) def _render( @@ -203,13 +220,14 @@ def _render( # not a core dump painter.end() cls._save_buffer(buffer, filename) + return cls._meta_data(buffer) if isinstance(object, GraphicsItem): - save_pyqtgraph() + return save_pyqtgraph() elif isinstance(object, QGraphicsScene): - save_scene() + return save_scene() elif isinstance(object, QWidget): # this includes QGraphicsView - save_widget() + return save_widget() else: raise TypeError(f"{cls.__name__} " f"cannot imagine {type(object).__name__}") @@ -218,7 +236,7 @@ def _render( def write(cls, filename, scene): if type(scene) == dict: scene = scene['scene'] - cls.write_image(filename, scene) + return cls.write_image(filename, scene) @classproperty def img_writers(cls): # type: () -> Mapping[str, Type[ImgFormat]] @@ -260,7 +278,11 @@ def _setup_painter(cls, painter, object, source_rect, buffer): @staticmethod def _save_buffer(buffer, filename): - buffer.save(filename, "png") + image = buffer.toImage() + dpm = int(2835 * image.devicePixelRatio()) + image.setDotsPerMeterX(dpm) + image.setDotsPerMeterY(dpm) + image.save(filename, "png") @staticmethod def _get_exporter(): @@ -300,6 +322,9 @@ def export(self, fileName=None, toBytes=False, copy=False): QtGui.QImage.Format.Format_ARGB32) self.png.fill(self.params['background']) self.png.setDevicePixelRatio(self.ratio) + dpm = int(2835 * self.ratio) + self.png.setDotsPerMeterX(dpm) + self.png.setDotsPerMeterY(dpm) ## set resolution of image: origTargetRect = self.getTargetRect() @@ -344,10 +369,11 @@ def export(self, fileName=None, toBytes=False, copy=False): return PngExporter - @staticmethod - def _export(exporter, filename): + @classmethod + def _export(cls, exporter, filename): buffer = exporter.export(toBytes=True) buffer.save(filename, "png") + return cls._meta_data(buffer) class ClipboardFormat(PngFormat): @@ -355,9 +381,16 @@ class ClipboardFormat(PngFormat): DESCRIPTION = 'System Clipboard' PRIORITY = 50 - @staticmethod - def _save_buffer(buffer, _): - QApplication.clipboard().setPixmap(buffer) + @classmethod + def _save_buffer(cls, buffer, _): + meta_data = cls._meta_data(buffer) + image = buffer.toImage() + if meta_data is not None: + ratio = meta_data.get("pixel_ratio", 1) + dpm = int(2835 * ratio) + image.setDotsPerMeterX(dpm) + image.setDotsPerMeterY(dpm) + QApplication.clipboard().setImage(image) @staticmethod def _export(exporter, _): diff --git a/orangewidget/report/report.py b/orangewidget/report/report.py index 15f8c1bb8..0ba897ce1 100644 --- a/orangewidget/report/report.py +++ b/orangewidget/report/report.py @@ -627,13 +627,19 @@ def get_html_img( byte_array = QByteArray() filename = QBuffer(byte_array) filename.open(QIODevice.WriteOnly) - PngFormat.write(filename, scene) + img_data = PngFormat.write(filename, scene) + img_encoded = byte_array.toBase64().data().decode("utf-8") - return ''.format( - ("" if max_height is None - else 'style="max-height: {}px"'.format(max_height)), - img_encoded - ) + + style_opts = [] + if max_height is not None: + style_opts.append(f"max-height: {max_height}px") + if img_data is not None: + ratio = img_data.get("pixel_ratio", 1) + if ratio != 1: + style_opts.append(f"zoom: {1 / ratio:.1f}") + style = f' style="{"; ".join(style_opts)}"' if style_opts else '' + return f'' def get_icon_html(icon: QIcon, size: QSize) -> str: