From ddc3728e2734300550e6d825b531546f5e76a87d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Douglas=20Samuel=20Gon=C3=A7alves?= Date: Wed, 12 Nov 2025 18:34:19 -0300 Subject: [PATCH 1/4] FIX: Implement device pixel ratio handling for proper scaling on Retina/HiDPI displays --- TutorialMaker/Lib/Annotations.py | 123 +++++---------------------- TutorialMaker/Lib/TutorialGUI.py | 4 + TutorialMaker/Lib/TutorialPainter.py | 37 +++++--- TutorialMaker/Lib/TutorialUtils.py | 10 +++ 4 files changed, 59 insertions(+), 115 deletions(-) diff --git a/TutorialMaker/Lib/Annotations.py b/TutorialMaker/Lib/Annotations.py index 85e767c..c0a7095 100644 --- a/TutorialMaker/Lib/Annotations.py +++ b/TutorialMaker/Lib/Annotations.py @@ -112,13 +112,15 @@ def penConfig(self, color, fontSize, thickness, brush = None, pen = None): self.fontSize = fontSize pass - def draw(self, painter : qt.QPainter = None, pen : qt.QPen = None, brush :qt.QBrush = None): + def draw(self, painter : qt.QPainter = None, pen : qt.QPen = None, brush :qt.QBrush = None, devicePixelRatio : float = 1.0): #Maybe we can organize this better - targetPos = [self.target["position"][0] - self.annotationOffset[0] + self.offsetX, - self.target["position"][1] - self.annotationOffset[1] + self.offsetY] + # Apply device pixel ratio correction for proper scaling on Retina/HiDPI displays + # Coordinates were multiplied by DPR during capture, so we divide here for drawing + targetPos = [(self.target["position"][0] - self.annotationOffset[0] + self.offsetX) / devicePixelRatio, + (self.target["position"][1] - self.annotationOffset[1] + self.offsetY) / devicePixelRatio] #Might as well do this then - targetSize = self.target["size"] + targetSize = [self.target["size"][0] / devicePixelRatio, self.target["size"][1] / devicePixelRatio] targetCenter = [targetPos[0] + targetSize[0]/2, @@ -406,6 +408,10 @@ def __init__(self, BackgroundImage : qt.QPixmap, Metadata : dict, Annotations : self.SlideLayout = "Screenshot" self.SlideTitle = "" self.SlideBody = "" + + # Store device pixel ratio for proper scaling on Retina/HiDPI displays + # Default to 1.0 for backward compatibility + self.devicePixelRatio = 1.0 pass def AddAnnotation(self, annotation : Annotation): @@ -420,8 +426,11 @@ def FindWidgetsAtPos(self, posX, posY): posY += self.windowOffset[1] for widget in self.metadata: - rectX, rectY = widget["position"] - rectWidth, rectHeight = widget["size"] + # Apply device pixel ratio correction when checking widget positions + rectX = widget["position"][0] / self.devicePixelRatio + rectY = widget["position"][1] / self.devicePixelRatio + rectWidth = widget["size"][0] / self.devicePixelRatio + rectHeight = widget["size"][1] / self.devicePixelRatio if rectX <= posX <= rectX + rectWidth and rectY <= posY <= rectY + rectHeight: results.append(widget) return results @@ -481,101 +490,7 @@ def Draw(self): pen = qt.QPen() brush = qt.QBrush() for annotation in self.annotations: - annotation.draw(painter, pen, brush) - painter.end() - - def __init__(self, BackgroundImage : qt.QPixmap, Metadata : dict, Annotations : list[Annotation] = None, WindowOffset : list[int] = None): - - self.image = BackgroundImage - self.outputImage = self.image.copy() - self.metadata = Metadata - if Annotations is None: - Annotations = [] - if WindowOffset is None: - WindowOffset = [0,0] - self.windowOffset = WindowOffset - self.annotations = Annotations - self.Active = True - - self.SlideLayout = "Screenshot" - self.SlideTitle = "" - self.SlideBody = "" - pass - - def AddAnnotation(self, annotation : Annotation): - annotation.setOffset(self.windowOffset) - self.annotations.append(annotation) - pass - - def FindWidgetsAtPos(self, posX, posY): - results = [] - - posX += self.windowOffset[0] - posY += self.windowOffset[1] - - for widget in self.metadata: - rectX, rectY = widget["position"] - rectWidth, rectHeight = widget["size"] - if rectX <= posX <= rectX + rectWidth and rectY <= posY <= rectY + rectHeight: - results.append(widget) - return results - - def FindAnnotationsAtPos(self, posX, posY): - results = [] - - for annotation in self.annotations: - rectX, rectY = annotation.boundingBoxTopLeft - rectWidth, rectHeight = annotation.getSelectionBoundingBoxSize() - if rectX <= posX <= rectX + rectWidth and rectY <= posY <= rectY + rectHeight: - results.append(annotation) - - results.sort(reverse=True, key= lambda x: x.getSelectionBoundingBoxSize()[0]*x.getSelectionBoundingBoxSize()[1]) - return results - - - def MapScreenToImage(self, qPos : qt.QPoint, qLabel : qt.QLabel): - imageSizeX = self.image.width() - imageSizeY = self.image.height() - - labelWidth = qLabel.width - labelHeight = qLabel.height - - x = Util.mapFromTo(qPos.x(), 0, labelWidth, 0, imageSizeX) - y = Util.mapFromTo(qPos.y(), 0, labelHeight, 0, imageSizeY) - - return [x,y] - - def MapImageToScreen(self, qPos : qt.QPoint, qLabel : qt.QLabel): - imageSizeX = self.image.width() - imageSizeY = self.image.height() - - labelWidth = qLabel.width - labelHeight = qLabel.height - - x = Util.mapFromTo(qPos.x(), 0, imageSizeX, 0, labelWidth) - y = Util.mapFromTo(qPos.y(), 0, imageSizeY, 0, labelHeight) - - return [x,y] - - def GetResized(self, resizeX : float = 0, resizeY : float = 0, keepAspectRatio=False) -> qt.QPixmap: - if resizeX <= 0 or resizeY <= 0: - return self.outputImage - if keepAspectRatio: - self.outputImage.scaled(resizeX, resizeY, qt.Qt.KeepAspectRatio, qt.Qt.SmoothTransformation) - return self.outputImage.scaled(resizeX, resizeY,qt.Qt.IgnoreAspectRatio, qt.Qt.SmoothTransformation) - - def ReDraw(self): - del self.outputImage - self.outputImage = self.image.copy() - self.Draw() - - def Draw(self): - painter = qt.QPainter(self.outputImage) - painter.setRenderHint(qt.QPainter.Antialiasing, True) - pen = qt.QPen() - brush = qt.QBrush() - for annotation in self.annotations: - annotation.draw(painter, pen, brush) + annotation.draw(painter, pen, brush, self.devicePixelRatio) painter.end() class AnnotatedTutorial: @@ -616,10 +531,12 @@ def LoadAnnotatedTutorial(path): slideImage : qt.QImage = None tsParser = TutorialScreenshot() + devicePixelRatio = 1.0 # Default for backward compatibility if slideData["SlideLayout"] == "Screenshot": try: tsParser.metadata = rawStepPath + ".json" slideMetadata = tsParser.getWidgets() + devicePixelRatio = tsParser.getDevicePixelRatio() slideImage = qt.QImage(rawStepPath + ".png") except FileNotFoundError: @@ -631,6 +548,9 @@ def LoadAnnotatedTutorial(path): continue tsParser.metadata = f"{stepPath}/{content}" slideMetadata.extend(tsParser.getWidgets()) + # Get DPR from the first metadata file found + if devicePixelRatio == 1.0: + devicePixelRatio = tsParser.getDevicePixelRatio() slideImage = qt.QImage(f"{outputFolder}/Annotations/{slideData['ImagePath']}") else: @@ -661,6 +581,7 @@ def LoadAnnotatedTutorial(path): annotation.PERSISTENT = True annotations.append(annotation) annotatedSlide = AnnotatorSlide(qt.QPixmap.fromImage(slideImage), slideMetadata, annotations) + annotatedSlide.devicePixelRatio = devicePixelRatio # Set the DPR for this slide annotatedSlide.SlideTitle = textDict.get(slideData["SlideTitle"], "") annotatedSlide.SlideBody = textDict.get(slideData["SlideDesc"], "") annotatedSlide.SlideLayout = slideData["SlideLayout"] diff --git a/TutorialMaker/Lib/TutorialGUI.py b/TutorialMaker/Lib/TutorialGUI.py index 81a6329..2477c69 100644 --- a/TutorialMaker/Lib/TutorialGUI.py +++ b/TutorialMaker/Lib/TutorialGUI.py @@ -457,6 +457,8 @@ def loadImagesAndMetadata(self, tutorialData): #Main window try: annotatorSlide = AnnotatorSlide(screenshots[0].getImage(), screenshots[0].getWidgets()) + # Set device pixel ratio for proper scaling on Retina/HiDPI displays + annotatorSlide.devicePixelRatio = screenshots[0].getDevicePixelRatio() stepWidget.AddStepWindows(annotatorSlide) except Exception: print(f"ERROR: Annotator Failed to add top level window in step:{stepIndex}, loadImagesAndMetadata") @@ -468,6 +470,8 @@ def loadImagesAndMetadata(self, tutorialData): annotatorSlide = AnnotatorSlide(screenshot.getImage(), screenshot.getWidgets(), WindowOffset=screenshot.getWidgets()[0]["position"]) + # Set device pixel ratio for proper scaling on Retina/HiDPI displays + annotatorSlide.devicePixelRatio = screenshot.getDevicePixelRatio() stepWidget.AddStepWindows(annotatorSlide) # noqa: F821 except Exception: print(f"ERROR: Annotator Failed to add window in step:{stepIndex}, loadImagesAndMetadata") diff --git a/TutorialMaker/Lib/TutorialPainter.py b/TutorialMaker/Lib/TutorialPainter.py index 00d6e4b..0a23c27 100644 --- a/TutorialMaker/Lib/TutorialPainter.py +++ b/TutorialMaker/Lib/TutorialPainter.py @@ -519,7 +519,7 @@ def save_to_png(self, filename): # TODO: In that moment we will remove the translation and only show in English # after define the infrastructre with Weblate or GitHub we will use community translation - def painter(self, metadata, screenshotData, language): + def painter(self, metadata, screenshotData, language, devicePixelRatio=1.0): """ Paint annotations on the image based on metadata and screenshot data. @@ -528,6 +528,7 @@ def painter(self, metadata, screenshotData, language): - metadata (dict): Metadata containing annotations information. - screenshotData (list): List of dictionaries containing widget information. - language (str): Language used for annotations (not used in the method). + - devicePixelRatio (float): Device pixel ratio from the screenshot capture (default 1.0). Outputs: None @@ -537,6 +538,7 @@ def painter(self, metadata, screenshotData, language): arrows, or click marks on the image based on the annotation type. It uses screenshot data to determine positions and sizes of widgets. The annotations are drawn with specified colors, text, font sizes, and pen widths. + The devicePixelRatio parameter ensures correct scaling on Retina/HiDPI displays. """ # Find corresponding widget data in screenshotData @@ -549,10 +551,12 @@ def painter(self, metadata, screenshotData, language): for widget in screenshotData: if widget["path"] == item["path"]: - widgetPosX = widget["position"][0] - widgetPosY = widget["position"][1] - widgetSizeX = widget["size"][0] - widgetSizeY = widget["size"][1] + # Apply device pixel ratio correction for proper scaling on Retina/HiDPI displays + # Coordinates were multiplied by DPR during capture, so we divide here for drawing + widgetPosX = widget["position"][0] / devicePixelRatio + widgetPosY = widget["position"][1] / devicePixelRatio + widgetSizeX = widget["size"][0] / devicePixelRatio + widgetSizeY = widget["size"][1] / devicePixelRatio text_ann = item["labelText"] if item['type'] == 'rectangle': @@ -568,10 +572,11 @@ def painter(self, metadata, screenshotData, language): pen_style=qt.Qt.SolidLine) elif item['type'] == 'arrow': - self.draw_arrow(start_x= item["direction_draw"][0], - start_y= item["direction_draw"][1], - end_x= item["direction_draw"][2], - end_y= item["direction_draw"][3], + # Apply device pixel ratio correction to arrow coordinates + self.draw_arrow(start_x= item["direction_draw"][0] / devicePixelRatio, + start_y= item["direction_draw"][1] / devicePixelRatio, + end_x= item["direction_draw"][2] / devicePixelRatio, + end_y= item["direction_draw"][3] / devicePixelRatio, color=tuple(map(int, item["color"].split(', '))), pen_width=6, text=text_ann, @@ -579,8 +584,9 @@ def painter(self, metadata, screenshotData, language): text_color=qt.Qt.black) elif item['type'] == 'clickMark': - self.draw_click(x=widgetPosX + (widgetSizeX / 2), - y=widgetPosY + (widgetSizeY / 2), + # Apply device pixel ratio correction to click mark position + self.draw_click(x=(widgetPosX + (widgetSizeX / 2)), + y=(widgetPosY + (widgetSizeY / 2)), text=text_ann, font_size=int(item['fontSize']), text_color=qt.Qt.black) @@ -619,11 +625,14 @@ def StartPaint(path,ListPositionWhite, ListoTotalImages): if(cont < len(ListPositionWhite)-1): cont = cont + 1 else: - screenshot = tutorial.steps[ListoTotalImages[i]].getImage() - screenshotData = tutorial.steps[ListoTotalImages[i]].getWidgets() + tutorialStep = tutorial.steps[ListoTotalImages[i]] + screenshot = tutorialStep.getImage() + screenshotData = tutorialStep.getWidgets() + devicePixelRatio = tutorialStep.getDevicePixelRatio() + # Load the image image_drawer.load_image(screenshot) - image_drawer.painter(OutputAnnotator[annotateSteps], screenshotData, 'es') + image_drawer.painter(OutputAnnotator[annotateSteps], screenshotData, 'es', devicePixelRatio) # Save the view to a PNG file with a dynamic path image_drawer.save_to_png(TutorialUtils.get_module_basepath("TutorialMaker") + '/Outputs/Translation/output_image_' + str(i) + '.png') diff --git a/TutorialMaker/Lib/TutorialUtils.py b/TutorialMaker/Lib/TutorialUtils.py index e79bbb1..d34f21b 100644 --- a/TutorialMaker/Lib/TutorialUtils.py +++ b/TutorialMaker/Lib/TutorialUtils.py @@ -727,6 +727,8 @@ def saveScreenshot(self, filename, window): def saveAllWidgetsData(self, filename, window): data = {} + # Save the device pixel ratio for proper scaling on different platforms + data["_devicePixelRatio"] = slicer.app.desktop().devicePixelRatioF() widgets = Util.getOnScreenWidgets(window) for index in range(len(widgets)): try: @@ -822,8 +824,16 @@ def getWidgets(self): widgets = [] nWidgets = JSONHandler.parseJSON(self.metadata) for keys in nWidgets: + # Skip metadata keys that start with underscore + if isinstance(keys, str) and keys.startswith("_"): + continue widgets.append(nWidgets[keys]) return widgets + + def getDevicePixelRatio(self): + """Get the device pixel ratio saved with this screenshot, defaults to 1.0""" + nWidgets = JSONHandler.parseJSON(self.metadata) + return nWidgets.get("_devicePixelRatio", 1.0) # TODO: REMOVE THIS, DEPRECATED class JSONHandler: From c220f83aa967b7b39906bbe200848e14f8d5f153 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Douglas=20Samuel=20Gon=C3=A7alves?= Date: Wed, 12 Nov 2025 19:47:44 -0300 Subject: [PATCH 2/4] FIX: Refactor device pixel ratio handling for proper scaling on Retina/HiDPI displays --- TutorialMaker/Lib/Annotations.py | 30 ++++++++++++------------ TutorialMaker/Lib/TutorialGUI.py | 6 ++--- TutorialMaker/Lib/TutorialPainter.py | 34 ++++++++++++---------------- TutorialMaker/Lib/TutorialUtils.py | 7 +++++- 4 files changed, 38 insertions(+), 39 deletions(-) diff --git a/TutorialMaker/Lib/Annotations.py b/TutorialMaker/Lib/Annotations.py index c0a7095..52bef2e 100644 --- a/TutorialMaker/Lib/Annotations.py +++ b/TutorialMaker/Lib/Annotations.py @@ -112,15 +112,14 @@ def penConfig(self, color, fontSize, thickness, brush = None, pen = None): self.fontSize = fontSize pass - def draw(self, painter : qt.QPainter = None, pen : qt.QPen = None, brush :qt.QBrush = None, devicePixelRatio : float = 1.0): + def draw(self, painter : qt.QPainter = None, pen : qt.QPen = None, brush :qt.QBrush = None): #Maybe we can organize this better - # Apply device pixel ratio correction for proper scaling on Retina/HiDPI displays - # Coordinates were multiplied by DPR during capture, so we divide here for drawing - targetPos = [(self.target["position"][0] - self.annotationOffset[0] + self.offsetX) / devicePixelRatio, - (self.target["position"][1] - self.annotationOffset[1] + self.offsetY) / devicePixelRatio] + # Device pixel ratio is handled by QPixmap.setDevicePixelRatio, no manual scaling needed + targetPos = [self.target["position"][0] - self.annotationOffset[0] + self.offsetX, + self.target["position"][1] - self.annotationOffset[1] + self.offsetY] #Might as well do this then - targetSize = [self.target["size"][0] / devicePixelRatio, self.target["size"][1] / devicePixelRatio] + targetSize = self.target["size"] targetCenter = [targetPos[0] + targetSize[0]/2, @@ -426,11 +425,9 @@ def FindWidgetsAtPos(self, posX, posY): posY += self.windowOffset[1] for widget in self.metadata: - # Apply device pixel ratio correction when checking widget positions - rectX = widget["position"][0] / self.devicePixelRatio - rectY = widget["position"][1] / self.devicePixelRatio - rectWidth = widget["size"][0] / self.devicePixelRatio - rectHeight = widget["size"][1] / self.devicePixelRatio + # Device pixel ratio is handled by QPixmap.setDevicePixelRatio + rectX, rectY = widget["position"] + rectWidth, rectHeight = widget["size"] if rectX <= posX <= rectX + rectWidth and rectY <= posY <= rectY + rectHeight: results.append(widget) return results @@ -490,7 +487,7 @@ def Draw(self): pen = qt.QPen() brush = qt.QBrush() for annotation in self.annotations: - annotation.draw(painter, pen, brush, self.devicePixelRatio) + annotation.draw(painter, pen, brush) painter.end() class AnnotatedTutorial: @@ -580,8 +577,13 @@ def LoadAnnotatedTutorial(path): ) annotation.PERSISTENT = True annotations.append(annotation) - annotatedSlide = AnnotatorSlide(qt.QPixmap.fromImage(slideImage), slideMetadata, annotations) - annotatedSlide.devicePixelRatio = devicePixelRatio # Set the DPR for this slide + + # Create pixmap and set device pixel ratio for proper scaling + pixmap = qt.QPixmap.fromImage(slideImage) + pixmap.setDevicePixelRatio(devicePixelRatio) + + annotatedSlide = AnnotatorSlide(pixmap, slideMetadata, annotations) + annotatedSlide.devicePixelRatio = devicePixelRatio # Store for reference annotatedSlide.SlideTitle = textDict.get(slideData["SlideTitle"], "") annotatedSlide.SlideBody = textDict.get(slideData["SlideDesc"], "") annotatedSlide.SlideLayout = slideData["SlideLayout"] diff --git a/TutorialMaker/Lib/TutorialGUI.py b/TutorialMaker/Lib/TutorialGUI.py index 2477c69..968c100 100644 --- a/TutorialMaker/Lib/TutorialGUI.py +++ b/TutorialMaker/Lib/TutorialGUI.py @@ -457,8 +457,7 @@ def loadImagesAndMetadata(self, tutorialData): #Main window try: annotatorSlide = AnnotatorSlide(screenshots[0].getImage(), screenshots[0].getWidgets()) - # Set device pixel ratio for proper scaling on Retina/HiDPI displays - annotatorSlide.devicePixelRatio = screenshots[0].getDevicePixelRatio() + # Device pixel ratio is already set on the pixmap by getImage() stepWidget.AddStepWindows(annotatorSlide) except Exception: print(f"ERROR: Annotator Failed to add top level window in step:{stepIndex}, loadImagesAndMetadata") @@ -470,8 +469,7 @@ def loadImagesAndMetadata(self, tutorialData): annotatorSlide = AnnotatorSlide(screenshot.getImage(), screenshot.getWidgets(), WindowOffset=screenshot.getWidgets()[0]["position"]) - # Set device pixel ratio for proper scaling on Retina/HiDPI displays - annotatorSlide.devicePixelRatio = screenshot.getDevicePixelRatio() + # Device pixel ratio is already set on the pixmap by getImage() stepWidget.AddStepWindows(annotatorSlide) # noqa: F821 except Exception: print(f"ERROR: Annotator Failed to add window in step:{stepIndex}, loadImagesAndMetadata") diff --git a/TutorialMaker/Lib/TutorialPainter.py b/TutorialMaker/Lib/TutorialPainter.py index 0a23c27..fd1d348 100644 --- a/TutorialMaker/Lib/TutorialPainter.py +++ b/TutorialMaker/Lib/TutorialPainter.py @@ -519,7 +519,7 @@ def save_to_png(self, filename): # TODO: In that moment we will remove the translation and only show in English # after define the infrastructre with Weblate or GitHub we will use community translation - def painter(self, metadata, screenshotData, language, devicePixelRatio=1.0): + def painter(self, metadata, screenshotData, language): """ Paint annotations on the image based on metadata and screenshot data. @@ -528,7 +528,6 @@ def painter(self, metadata, screenshotData, language, devicePixelRatio=1.0): - metadata (dict): Metadata containing annotations information. - screenshotData (list): List of dictionaries containing widget information. - language (str): Language used for annotations (not used in the method). - - devicePixelRatio (float): Device pixel ratio from the screenshot capture (default 1.0). Outputs: None @@ -538,7 +537,7 @@ def painter(self, metadata, screenshotData, language, devicePixelRatio=1.0): arrows, or click marks on the image based on the annotation type. It uses screenshot data to determine positions and sizes of widgets. The annotations are drawn with specified colors, text, font sizes, and pen widths. - The devicePixelRatio parameter ensures correct scaling on Retina/HiDPI displays. + Device pixel ratio is handled by QPixmap.setDevicePixelRatio for proper scaling. """ # Find corresponding widget data in screenshotData @@ -551,12 +550,10 @@ def painter(self, metadata, screenshotData, language, devicePixelRatio=1.0): for widget in screenshotData: if widget["path"] == item["path"]: - # Apply device pixel ratio correction for proper scaling on Retina/HiDPI displays - # Coordinates were multiplied by DPR during capture, so we divide here for drawing - widgetPosX = widget["position"][0] / devicePixelRatio - widgetPosY = widget["position"][1] / devicePixelRatio - widgetSizeX = widget["size"][0] / devicePixelRatio - widgetSizeY = widget["size"][1] / devicePixelRatio + widgetPosX = widget["position"][0] + widgetPosY = widget["position"][1] + widgetSizeX = widget["size"][0] + widgetSizeY = widget["size"][1] text_ann = item["labelText"] if item['type'] == 'rectangle': @@ -572,11 +569,10 @@ def painter(self, metadata, screenshotData, language, devicePixelRatio=1.0): pen_style=qt.Qt.SolidLine) elif item['type'] == 'arrow': - # Apply device pixel ratio correction to arrow coordinates - self.draw_arrow(start_x= item["direction_draw"][0] / devicePixelRatio, - start_y= item["direction_draw"][1] / devicePixelRatio, - end_x= item["direction_draw"][2] / devicePixelRatio, - end_y= item["direction_draw"][3] / devicePixelRatio, + self.draw_arrow(start_x= item["direction_draw"][0], + start_y= item["direction_draw"][1], + end_x= item["direction_draw"][2], + end_y= item["direction_draw"][3], color=tuple(map(int, item["color"].split(', '))), pen_width=6, text=text_ann, @@ -584,9 +580,8 @@ def painter(self, metadata, screenshotData, language, devicePixelRatio=1.0): text_color=qt.Qt.black) elif item['type'] == 'clickMark': - # Apply device pixel ratio correction to click mark position - self.draw_click(x=(widgetPosX + (widgetSizeX / 2)), - y=(widgetPosY + (widgetSizeY / 2)), + self.draw_click(x=widgetPosX + (widgetSizeX / 2), + y=widgetPosY + (widgetSizeY / 2), text=text_ann, font_size=int(item['fontSize']), text_color=qt.Qt.black) @@ -628,11 +623,10 @@ def StartPaint(path,ListPositionWhite, ListoTotalImages): tutorialStep = tutorial.steps[ListoTotalImages[i]] screenshot = tutorialStep.getImage() screenshotData = tutorialStep.getWidgets() - devicePixelRatio = tutorialStep.getDevicePixelRatio() - # Load the image + # Load the image (devicePixelRatio is already set on the pixmap) image_drawer.load_image(screenshot) - image_drawer.painter(OutputAnnotator[annotateSteps], screenshotData, 'es', devicePixelRatio) + image_drawer.painter(OutputAnnotator[annotateSteps], screenshotData, 'es') # Save the view to a PNG file with a dynamic path image_drawer.save_to_png(TutorialUtils.get_module_basepath("TutorialMaker") + '/Outputs/Translation/output_image_' + str(i) + '.png') diff --git a/TutorialMaker/Lib/TutorialUtils.py b/TutorialMaker/Lib/TutorialUtils.py index d34f21b..609f6cb 100644 --- a/TutorialMaker/Lib/TutorialUtils.py +++ b/TutorialMaker/Lib/TutorialUtils.py @@ -819,7 +819,12 @@ def __init__(self, screenshot="", metadata=""): def getImage(self): image = qt.QImage(self.screenshot) - return qt.QPixmap.fromImage(image) + pixmap = qt.QPixmap.fromImage(image) + # Set the device pixel ratio so Qt scales the pixmap correctly on HiDPI displays + # This ensures coordinates and image are in the same coordinate system + dpr = self.getDevicePixelRatio() + pixmap.setDevicePixelRatio(dpr) + return pixmap def getWidgets(self): widgets = [] nWidgets = JSONHandler.parseJSON(self.metadata) From 1b130359dca6dc342ed297a7f8049ff0a358fb04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Douglas=20Samuel=20Gon=C3=A7alves?= Date: Wed, 12 Nov 2025 20:04:08 -0300 Subject: [PATCH 3/4] FIX: Refactor device pixel ratio handling for improved scaling on Retina/HiDPI displays --- TutorialMaker/Lib/Annotations.py | 15 ++++++--------- TutorialMaker/Lib/TutorialGUI.py | 2 -- TutorialMaker/Lib/TutorialPainter.py | 5 ----- TutorialMaker/Lib/TutorialUtils.py | 13 ++++++++----- 4 files changed, 14 insertions(+), 21 deletions(-) diff --git a/TutorialMaker/Lib/Annotations.py b/TutorialMaker/Lib/Annotations.py index 52bef2e..f6deeca 100644 --- a/TutorialMaker/Lib/Annotations.py +++ b/TutorialMaker/Lib/Annotations.py @@ -113,12 +113,9 @@ def penConfig(self, color, fontSize, thickness, brush = None, pen = None): pass def draw(self, painter : qt.QPainter = None, pen : qt.QPen = None, brush :qt.QBrush = None): - #Maybe we can organize this better - # Device pixel ratio is handled by QPixmap.setDevicePixelRatio, no manual scaling needed targetPos = [self.target["position"][0] - self.annotationOffset[0] + self.offsetX, self.target["position"][1] - self.annotationOffset[1] + self.offsetY] - #Might as well do this then targetSize = self.target["size"] @@ -408,8 +405,6 @@ def __init__(self, BackgroundImage : qt.QPixmap, Metadata : dict, Annotations : self.SlideTitle = "" self.SlideBody = "" - # Store device pixel ratio for proper scaling on Retina/HiDPI displays - # Default to 1.0 for backward compatibility self.devicePixelRatio = 1.0 pass @@ -425,7 +420,6 @@ def FindWidgetsAtPos(self, posX, posY): posY += self.windowOffset[1] for widget in self.metadata: - # Device pixel ratio is handled by QPixmap.setDevicePixelRatio rectX, rectY = widget["position"] rectWidth, rectHeight = widget["size"] if rectX <= posX <= rectX + rectWidth and rectY <= posY <= rectY + rectHeight: @@ -578,12 +572,15 @@ def LoadAnnotatedTutorial(path): annotation.PERSISTENT = True annotations.append(annotation) - # Create pixmap and set device pixel ratio for proper scaling pixmap = qt.QPixmap.fromImage(slideImage) - pixmap.setDevicePixelRatio(devicePixelRatio) + if devicePixelRatio > 1.0: + logicalWidth = int(pixmap.width() / devicePixelRatio) + logicalHeight = int(pixmap.height() / devicePixelRatio) + pixmap = pixmap.scaled(logicalWidth, logicalHeight, qt.Qt.KeepAspectRatio, qt.Qt.SmoothTransformation) + pixmap.setDevicePixelRatio(1.0) annotatedSlide = AnnotatorSlide(pixmap, slideMetadata, annotations) - annotatedSlide.devicePixelRatio = devicePixelRatio # Store for reference + annotatedSlide.devicePixelRatio = 1.0 annotatedSlide.SlideTitle = textDict.get(slideData["SlideTitle"], "") annotatedSlide.SlideBody = textDict.get(slideData["SlideDesc"], "") annotatedSlide.SlideLayout = slideData["SlideLayout"] diff --git a/TutorialMaker/Lib/TutorialGUI.py b/TutorialMaker/Lib/TutorialGUI.py index 968c100..81a6329 100644 --- a/TutorialMaker/Lib/TutorialGUI.py +++ b/TutorialMaker/Lib/TutorialGUI.py @@ -457,7 +457,6 @@ def loadImagesAndMetadata(self, tutorialData): #Main window try: annotatorSlide = AnnotatorSlide(screenshots[0].getImage(), screenshots[0].getWidgets()) - # Device pixel ratio is already set on the pixmap by getImage() stepWidget.AddStepWindows(annotatorSlide) except Exception: print(f"ERROR: Annotator Failed to add top level window in step:{stepIndex}, loadImagesAndMetadata") @@ -469,7 +468,6 @@ def loadImagesAndMetadata(self, tutorialData): annotatorSlide = AnnotatorSlide(screenshot.getImage(), screenshot.getWidgets(), WindowOffset=screenshot.getWidgets()[0]["position"]) - # Device pixel ratio is already set on the pixmap by getImage() stepWidget.AddStepWindows(annotatorSlide) # noqa: F821 except Exception: print(f"ERROR: Annotator Failed to add window in step:{stepIndex}, loadImagesAndMetadata") diff --git a/TutorialMaker/Lib/TutorialPainter.py b/TutorialMaker/Lib/TutorialPainter.py index fd1d348..7d8d539 100644 --- a/TutorialMaker/Lib/TutorialPainter.py +++ b/TutorialMaker/Lib/TutorialPainter.py @@ -517,8 +517,6 @@ def save_to_png(self, filename): else: print("Error: No view to save.") - # TODO: In that moment we will remove the translation and only show in English - # after define the infrastructre with Weblate or GitHub we will use community translation def painter(self, metadata, screenshotData, language): """ @@ -537,7 +535,6 @@ def painter(self, metadata, screenshotData, language): arrows, or click marks on the image based on the annotation type. It uses screenshot data to determine positions and sizes of widgets. The annotations are drawn with specified colors, text, font sizes, and pen widths. - Device pixel ratio is handled by QPixmap.setDevicePixelRatio for proper scaling. """ # Find corresponding widget data in screenshotData @@ -624,11 +621,9 @@ def StartPaint(path,ListPositionWhite, ListoTotalImages): screenshot = tutorialStep.getImage() screenshotData = tutorialStep.getWidgets() - # Load the image (devicePixelRatio is already set on the pixmap) image_drawer.load_image(screenshot) image_drawer.painter(OutputAnnotator[annotateSteps], screenshotData, 'es') - # Save the view to a PNG file with a dynamic path image_drawer.save_to_png(TutorialUtils.get_module_basepath("TutorialMaker") + '/Outputs/Translation/output_image_' + str(i) + '.png') imgSS = imgSS + 1 diff --git a/TutorialMaker/Lib/TutorialUtils.py b/TutorialMaker/Lib/TutorialUtils.py index 609f6cb..5191729 100644 --- a/TutorialMaker/Lib/TutorialUtils.py +++ b/TutorialMaker/Lib/TutorialUtils.py @@ -727,7 +727,6 @@ def saveScreenshot(self, filename, window): def saveAllWidgetsData(self, filename, window): data = {} - # Save the device pixel ratio for proper scaling on different platforms data["_devicePixelRatio"] = slicer.app.desktop().devicePixelRatioF() widgets = Util.getOnScreenWidgets(window) for index in range(len(widgets)): @@ -820,16 +819,20 @@ def __init__(self, screenshot="", metadata=""): def getImage(self): image = qt.QImage(self.screenshot) pixmap = qt.QPixmap.fromImage(image) - # Set the device pixel ratio so Qt scales the pixmap correctly on HiDPI displays - # This ensures coordinates and image are in the same coordinate system + dpr = self.getDevicePixelRatio() - pixmap.setDevicePixelRatio(dpr) + if dpr > 1.0: + logicalWidth = int(pixmap.width() / dpr) + logicalHeight = int(pixmap.height() / dpr) + pixmap = pixmap.scaled(logicalWidth, logicalHeight, qt.Qt.KeepAspectRatio, qt.Qt.SmoothTransformation) + + pixmap.setDevicePixelRatio(1.0) return pixmap def getWidgets(self): widgets = [] nWidgets = JSONHandler.parseJSON(self.metadata) + for keys in nWidgets: - # Skip metadata keys that start with underscore if isinstance(keys, str) and keys.startswith("_"): continue widgets.append(nWidgets[keys]) From 958da530c237292442215fcda2f4c69e71c5936f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Douglas=20Samuel=20Gon=C3=A7alves?= Date: Wed, 12 Nov 2025 20:30:44 -0300 Subject: [PATCH 4/4] FIX: Adjust widget positions and sizes based on device pixel ratio in TutorialScreenshot --- TutorialMaker/Lib/TutorialUtils.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/TutorialMaker/Lib/TutorialUtils.py b/TutorialMaker/Lib/TutorialUtils.py index 5191729..0755fbf 100644 --- a/TutorialMaker/Lib/TutorialUtils.py +++ b/TutorialMaker/Lib/TutorialUtils.py @@ -831,11 +831,19 @@ def getImage(self): def getWidgets(self): widgets = [] nWidgets = JSONHandler.parseJSON(self.metadata) + dpr = self.getDevicePixelRatio() for keys in nWidgets: if isinstance(keys, str) and keys.startswith("_"): continue - widgets.append(nWidgets[keys]) + + widget = nWidgets[keys].copy() if hasattr(nWidgets[keys], 'copy') else dict(nWidgets[keys]) + + if dpr > 1.0: + widget["position"] = [widget["position"][0] / dpr, widget["position"][1] / dpr] + widget["size"] = [widget["size"][0] / dpr, widget["size"][1] / dpr] + + widgets.append(widget) return widgets def getDevicePixelRatio(self):