1#!/usr/local/bin/python3.8 2# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:fdm=marker:ai 3 4 5__license__ = 'GPL v3' 6__copyright__ = '2012, Kovid Goyal <kovid at kovidgoyal.net>' 7__docformat__ = 'restructuredtext en' 8 9from qt.core import ( 10 QApplication, QCheckBox, QDialog, QDialogButtonBox, QHBoxLayout, QIcon, QImage, 11 QLabel, QPainter, QPalette, QPixmap, QScrollArea, QSize, QSizePolicy, 12 QSvgRenderer, Qt, QTransform, QUrl, QVBoxLayout, pyqtSignal 13) 14 15from calibre import fit_image 16from calibre.gui2 import ( 17 NO_URL_FORMATTING, choose_save_file, gprefs, max_available_height 18) 19 20 21def render_svg(widget, path): 22 img = QPixmap() 23 rend = QSvgRenderer() 24 if rend.load(path): 25 dpr = getattr(widget, 'devicePixelRatioF', widget.devicePixelRatio)() 26 sz = rend.defaultSize() 27 h = (max_available_height() - 50) 28 w = int(h * sz.height() / float(sz.width())) 29 pd = QImage(w * dpr, h * dpr, QImage.Format.Format_RGB32) 30 pd.fill(Qt.GlobalColor.white) 31 p = QPainter(pd) 32 rend.render(p) 33 p.end() 34 img = QPixmap.fromImage(pd) 35 img.setDevicePixelRatio(dpr) 36 return img 37 38 39class Label(QLabel): 40 41 toggle_fit = pyqtSignal() 42 43 def __init__(self, scrollarea): 44 super().__init__(scrollarea) 45 self.setBackgroundRole(QPalette.ColorRole.Text if QApplication.instance().is_dark_theme else QPalette.ColorRole.Base) 46 self.setSizePolicy(QSizePolicy.Policy.Ignored, QSizePolicy.Policy.Ignored) 47 self.setScaledContents(True) 48 self.default_cursor = self.cursor() 49 self.in_drag = False 50 self.prev_drag_position = None 51 self.scrollarea = scrollarea 52 53 @property 54 def is_pannable(self): 55 return self.scrollarea.verticalScrollBar().isVisible() or self.scrollarea.horizontalScrollBar().isVisible() 56 57 def mousePressEvent(self, ev): 58 if ev.button() == Qt.MouseButton.LeftButton and self.is_pannable: 59 self.setCursor(Qt.CursorShape.ClosedHandCursor) 60 self.in_drag = True 61 self.prev_drag_position = ev.globalPos() 62 return super().mousePressEvent(ev) 63 64 def mouseReleaseEvent(self, ev): 65 if ev.button() == Qt.MouseButton.LeftButton and self.in_drag: 66 self.setCursor(self.default_cursor) 67 self.in_drag = False 68 self.prev_drag_position = None 69 return super().mousePressEvent(ev) 70 71 def mouseMoveEvent(self, ev): 72 if self.prev_drag_position is not None: 73 p = self.prev_drag_position 74 self.prev_drag_position = pos = ev.globalPos() 75 self.dragged(pos.x() - p.x(), pos.y() - p.y()) 76 return super().mouseMoveEvent(ev) 77 78 def dragged(self, dx, dy): 79 h = self.scrollarea.horizontalScrollBar() 80 if h.isVisible(): 81 h.setValue(h.value() - dx) 82 v = self.scrollarea.verticalScrollBar() 83 if v.isVisible(): 84 v.setValue(v.value() - dy) 85 86 87class ScrollArea(QScrollArea): 88 89 toggle_fit = pyqtSignal() 90 91 def mouseDoubleClickEvent(self, ev): 92 if ev.button() == Qt.MouseButton.LeftButton: 93 self.toggle_fit.emit() 94 95 96class ImageView(QDialog): 97 98 def __init__(self, parent, current_img, current_url, geom_name='viewer_image_popup_geometry'): 99 QDialog.__init__(self) 100 self.current_image_name = '' 101 self.maximized_at_last_fullscreen = False 102 self.setWindowFlag(Qt.WindowType.WindowMinimizeButtonHint) 103 self.setWindowFlag(Qt.WindowType.WindowMaximizeButtonHint) 104 self.avail_geom = self.screen().availableGeometry() 105 self.current_img = current_img 106 self.current_url = current_url 107 self.factor = 1.0 108 self.geom_name = geom_name 109 110 self.scrollarea = sa = ScrollArea() 111 sa.setAlignment(Qt.AlignmentFlag.AlignHCenter | Qt.AlignmentFlag.AlignVCenter) 112 sa.setBackgroundRole(QPalette.ColorRole.Dark) 113 self.label = l = Label(sa) 114 sa.toggle_fit.connect(self.toggle_fit) 115 sa.setWidget(l) 116 117 self.bb = bb = QDialogButtonBox(QDialogButtonBox.StandardButton.Close) 118 bb.accepted.connect(self.accept) 119 bb.rejected.connect(self.reject) 120 self.zi_button = zi = bb.addButton(_('Zoom &in'), QDialogButtonBox.ButtonRole.ActionRole) 121 self.zo_button = zo = bb.addButton(_('Zoom &out'), QDialogButtonBox.ButtonRole.ActionRole) 122 self.save_button = so = bb.addButton(_('&Save as'), QDialogButtonBox.ButtonRole.ActionRole) 123 self.rotate_button = ro = bb.addButton(_('&Rotate'), QDialogButtonBox.ButtonRole.ActionRole) 124 self.fullscreen_button = fo = bb.addButton(_('&Full screen'), QDialogButtonBox.ButtonRole.ActionRole) 125 zi.setIcon(QIcon(I('plus.png'))) 126 zo.setIcon(QIcon(I('minus.png'))) 127 so.setIcon(QIcon(I('save.png'))) 128 ro.setIcon(QIcon(I('rotate-right.png'))) 129 fo.setIcon(QIcon(I('page.png'))) 130 zi.clicked.connect(self.zoom_in) 131 zo.clicked.connect(self.zoom_out) 132 so.clicked.connect(self.save_image) 133 ro.clicked.connect(self.rotate_image) 134 fo.setCheckable(True) 135 136 self.l = l = QVBoxLayout(self) 137 l.addWidget(sa) 138 self.h = h = QHBoxLayout() 139 h.setContentsMargins(0, 0, 0, 0) 140 l.addLayout(h) 141 self.fit_image = i = QCheckBox(_('&Fit image')) 142 i.setToolTip(_('Fit image inside the available space')) 143 i.setChecked(bool(gprefs.get('image_popup_fit_image'))) 144 i.stateChanged.connect(self.fit_changed) 145 h.addWidget(i), h.addStretch(), h.addWidget(bb) 146 if self.fit_image.isChecked(): 147 self.set_to_viewport_size() 148 geom = gprefs.get(self.geom_name) 149 if geom is not None: 150 self.restoreGeometry(geom) 151 fo.setChecked(self.isFullScreen()) 152 fo.toggled.connect(self.toggle_fullscreen) 153 154 def set_to_viewport_size(self): 155 page_size = self.scrollarea.size() 156 pw, ph = page_size.width() - 2, page_size.height() - 2 157 img_size = self.current_img.size() 158 iw, ih = img_size.width(), img_size.height() 159 scaled, nw, nh = fit_image(iw, ih, pw, ph) 160 if scaled: 161 self.factor = min(nw/iw, nh/ih) 162 img_size.setWidth(nw), img_size.setHeight(nh) 163 self.label.resize(img_size) 164 165 def resizeEvent(self, ev): 166 if self.fit_image.isChecked(): 167 self.set_to_viewport_size() 168 169 def factor_from_fit(self): 170 scaled_height = self.label.size().height() 171 actual_height = self.current_img.size().height() 172 return scaled_height / actual_height 173 174 def zoom_in(self): 175 if self.fit_image.isChecked(): 176 factor = self.factor_from_fit() 177 self.fit_image.setChecked(False) 178 self.factor = factor 179 self.factor *= 1.25 180 self.adjust_image(1.25) 181 182 def zoom_out(self): 183 if self.fit_image.isChecked(): 184 factor = self.factor_from_fit() 185 self.fit_image.setChecked(False) 186 self.factor = factor 187 self.factor *= 0.8 188 self.adjust_image(0.8) 189 190 def save_image(self): 191 filters=[('Images', ['png', 'jpeg', 'jpg'])] 192 f = choose_save_file(self, 'viewer image view save dialog', 193 _('Choose a file to save to'), filters=filters, 194 all_files=False, initial_filename=self.current_image_name or None) 195 if f: 196 from calibre.utils.img import save_image 197 save_image(self.current_img.toImage(), f) 198 199 def fit_changed(self): 200 fitted = bool(self.fit_image.isChecked()) 201 gprefs.set('image_popup_fit_image', fitted) 202 if self.fit_image.isChecked(): 203 self.set_to_viewport_size() 204 else: 205 self.factor = 1 206 self.adjust_image(1) 207 208 def toggle_fit(self): 209 self.fit_image.toggle() 210 211 def adjust_image(self, factor): 212 if self.fit_image.isChecked(): 213 self.set_to_viewport_size() 214 return 215 self.label.resize(self.factor * self.current_img.size()) 216 self.zi_button.setEnabled(self.factor <= 3) 217 self.zo_button.setEnabled(self.factor >= 0.3333) 218 self.adjust_scrollbars(factor) 219 220 def adjust_scrollbars(self, factor): 221 for sb in (self.scrollarea.horizontalScrollBar(), 222 self.scrollarea.verticalScrollBar()): 223 sb.setValue(int(factor*sb.value()) + int((factor - 1) * sb.pageStep()/2)) 224 225 def rotate_image(self): 226 pm = self.label.pixmap() 227 t = QTransform() 228 t.rotate(90) 229 pm = self.current_img = pm.transformed(t) 230 self.label.setPixmap(pm) 231 self.label.adjustSize() 232 if self.fit_image.isChecked(): 233 self.set_to_viewport_size() 234 else: 235 self.factor = 1 236 for sb in (self.scrollarea.horizontalScrollBar(), 237 self.scrollarea.verticalScrollBar()): 238 sb.setValue(0) 239 240 def __call__(self, use_exec=False): 241 geom = self.avail_geom 242 self.label.setPixmap(self.current_img) 243 self.label.adjustSize() 244 self.resize(QSize(int(geom.width()/2.5), geom.height()-50)) 245 geom = gprefs.get(self.geom_name, None) 246 if geom is not None: 247 QApplication.instance().safe_restore_geometry(self, geom) 248 try: 249 self.current_image_name = str(self.current_url.toString(NO_URL_FORMATTING)).rpartition('/')[-1] 250 except AttributeError: 251 self.current_image_name = self.current_url 252 reso = '' 253 if self.current_img and not self.current_img.isNull(): 254 reso = f'[{self.current_img.width()}x{self.current_img.height()}]' 255 title = _('Image: {name} {resolution}').format(name=self.current_image_name, resolution=reso) 256 self.setWindowTitle(title) 257 if use_exec: 258 self.exec() 259 else: 260 self.show() 261 262 def done(self, e): 263 gprefs[self.geom_name] = bytearray(self.saveGeometry()) 264 return QDialog.done(self, e) 265 266 def toggle_fullscreen(self): 267 on = not self.isFullScreen() 268 if on: 269 self.maximized_at_last_fullscreen = self.isMaximized() 270 self.showFullScreen() 271 else: 272 if self.maximized_at_last_fullscreen: 273 self.showMaximized() 274 else: 275 self.showNormal() 276 277 def wheelEvent(self, event): 278 d = event.angleDelta().y() 279 if abs(d) > 0 and not self.scrollarea.verticalScrollBar().isVisible(): 280 event.accept() 281 (self.zoom_out if d < 0 else self.zoom_in)() 282 283 284class ImagePopup: 285 286 def __init__(self, parent): 287 self.current_img = QPixmap() 288 self.current_url = QUrl() 289 self.parent = parent 290 self.dialogs = [] 291 292 def __call__(self): 293 if self.current_img.isNull(): 294 return 295 d = ImageView(self.parent, self.current_img, self.current_url) 296 self.dialogs.append(d) 297 d.finished.connect(self.cleanup, type=Qt.ConnectionType.QueuedConnection) 298 d() 299 300 def cleanup(self): 301 for d in tuple(self.dialogs): 302 if not d.isVisible(): 303 self.dialogs.remove(d) 304 305 306if __name__ == '__main__': 307 import sys 308 309 from calibre.gui2 import Application 310 app = Application([]) 311 p = QPixmap() 312 p.load(sys.argv[-1]) 313 u = QUrl.fromLocalFile(sys.argv[-1]) 314 d = ImageView(None, p, u) 315 d() 316 app.exec() 317