1#!/usr/local/bin/python3.8 2# vim:fileencoding=utf-8 3 4 5__license__ = 'GPL v3' 6__copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>' 7 8import sys, weakref 9from functools import wraps 10from io import BytesIO 11 12from qt.core import ( 13 QWidget, QPainter, QColor, QApplication, Qt, QPixmap, QRectF, QTransform, 14 QPointF, QPen, pyqtSignal, QUndoCommand, QUndoStack, QIcon, QImage, 15 QImageWriter) 16 17from calibre import fit_image 18from calibre.gui2 import error_dialog, pixmap_to_data 19from calibre.gui2.dnd import ( 20 image_extensions, dnd_has_extension, dnd_has_image, dnd_get_image, DownloadDialog) 21from calibre.gui2.tweak_book import capitalize 22from calibre.utils.imghdr import identify 23from calibre.utils.img import ( 24 remove_borders_from_image, gaussian_sharpen_image, gaussian_blur_image, image_to_data, despeckle_image, 25 normalize_image, oil_paint_image 26) 27 28 29def painter(func): 30 @wraps(func) 31 def ans(self, painter): 32 painter.save() 33 try: 34 return func(self, painter) 35 finally: 36 painter.restore() 37 return ans 38 39 40class SelectionState: 41 42 __slots__ = ('last_press_point', 'current_mode', 'rect', 'in_selection', 'drag_corner', 'dragging', 'last_drag_pos') 43 44 def __init__(self): 45 self.reset() 46 47 def reset(self, full=True): 48 self.last_press_point = None 49 if full: 50 self.current_mode = None 51 self.rect = None 52 self.in_selection = False 53 self.drag_corner = None 54 self.dragging = None 55 self.last_drag_pos = None 56 57 58class Command(QUndoCommand): 59 60 TEXT = '' 61 62 def __init__(self, canvas): 63 QUndoCommand.__init__(self, self.TEXT) 64 self.canvas_ref = weakref.ref(canvas) 65 self.before_image = i = canvas.current_image 66 if i is None: 67 raise ValueError('No image loaded') 68 if i.isNull(): 69 raise ValueError('Cannot perform operations on invalid images') 70 self.after_image = self(canvas) 71 72 def undo(self): 73 canvas = self.canvas_ref() 74 canvas.set_image(self.before_image) 75 76 def redo(self): 77 canvas = self.canvas_ref() 78 canvas.set_image(self.after_image) 79 80 81def get_selection_rect(img, sr, target): 82 ' Given selection rect return the corresponding rectangle in the underlying image as left, top, width, height ' 83 left_border = (abs(sr.left() - target.left())/target.width()) * img.width() 84 top_border = (abs(sr.top() - target.top())/target.height()) * img.height() 85 right_border = (abs(target.right() - sr.right())/target.width()) * img.width() 86 bottom_border = (abs(target.bottom() - sr.bottom())/target.height()) * img.height() 87 return left_border, top_border, img.width() - left_border - right_border, img.height() - top_border - bottom_border 88 89 90class Trim(Command): 91 92 ''' Remove the areas of the image outside the current selection. ''' 93 94 TEXT = _('Trim image') 95 96 def __call__(self, canvas): 97 img = canvas.current_image 98 target = canvas.target 99 sr = canvas.selection_state.rect 100 return img.copy(*map(int, get_selection_rect(img, sr, target))) 101 102 103class AutoTrim(Trim): 104 105 ''' Auto trim borders from the image ''' 106 TEXT = _('Auto-trim image') 107 108 def __call__(self, canvas): 109 return remove_borders_from_image(canvas.current_image) 110 111 112class Rotate(Command): 113 114 TEXT = _('Rotate image') 115 116 def __call__(self, canvas): 117 img = canvas.current_image 118 m = QTransform() 119 m.rotate(90) 120 return img.transformed(m, Qt.TransformationMode.SmoothTransformation) 121 122 123class Scale(Command): 124 125 TEXT = _('Resize image') 126 127 def __init__(self, width, height, canvas): 128 self.width, self.height = width, height 129 Command.__init__(self, canvas) 130 131 def __call__(self, canvas): 132 img = canvas.current_image 133 return img.scaled(self.width, self.height, transformMode=Qt.TransformationMode.SmoothTransformation) 134 135 136class Sharpen(Command): 137 138 TEXT = _('Sharpen image') 139 FUNC = 'sharpen' 140 141 def __init__(self, sigma, canvas): 142 self.sigma = sigma 143 Command.__init__(self, canvas) 144 145 def __call__(self, canvas): 146 return gaussian_sharpen_image(canvas.current_image, sigma=self.sigma) 147 148 149class Blur(Sharpen): 150 151 TEXT = _('Blur image') 152 FUNC = 'blur' 153 154 def __call__(self, canvas): 155 return gaussian_blur_image(canvas.current_image, sigma=self.sigma) 156 157 158class Oilify(Command): 159 160 TEXT = _('Make image look like an oil painting') 161 162 def __init__(self, radius, canvas): 163 self.radius = radius 164 Command.__init__(self, canvas) 165 166 def __call__(self, canvas): 167 return oil_paint_image(canvas.current_image, radius=self.radius) 168 169 170class Despeckle(Command): 171 172 TEXT = _('De-speckle image') 173 174 def __call__(self, canvas): 175 return despeckle_image(canvas.current_image) 176 177 178class Normalize(Command): 179 180 TEXT = _('Normalize image') 181 182 def __call__(self, canvas): 183 return normalize_image(canvas.current_image) 184 185 186class Replace(Command): 187 188 ''' Replace the current image with another image. If there is a selection, 189 only the region of the selection is replaced. ''' 190 191 def __init__(self, img, text, canvas): 192 self.after_image = img 193 self.TEXT = text 194 Command.__init__(self, canvas) 195 196 def __call__(self, canvas): 197 if canvas.has_selection and canvas.selection_state.rect is not None: 198 pimg = self.after_image 199 img = self.after_image = QImage(canvas.current_image) 200 rect = QRectF(*get_selection_rect(img, canvas.selection_state.rect, canvas.target)) 201 p = QPainter(img) 202 p.setRenderHint(QPainter.RenderHint.SmoothPixmapTransform, True) 203 p.drawImage(rect, pimg, QRectF(pimg.rect())) 204 p.end() 205 return self.after_image 206 207 208def imageop(func): 209 @wraps(func) 210 def ans(self, *args, **kwargs): 211 if self.original_image_data is None: 212 return error_dialog(self, _('No image'), _('No image loaded'), show=True) 213 if not self.is_valid: 214 return error_dialog(self, _('Invalid image'), _('The current image is not valid'), show=True) 215 QApplication.setOverrideCursor(Qt.CursorShape.BusyCursor) 216 try: 217 return func(self, *args, **kwargs) 218 finally: 219 QApplication.restoreOverrideCursor() 220 return ans 221 222 223class Canvas(QWidget): 224 225 BACKGROUND = QColor(60, 60, 60) 226 SHADE_COLOR = QColor(0, 0, 0, 180) 227 SELECT_PEN = QPen(QColor(Qt.GlobalColor.white)) 228 229 selection_state_changed = pyqtSignal(object) 230 selection_area_changed = pyqtSignal(object) 231 undo_redo_state_changed = pyqtSignal(object, object) 232 image_changed = pyqtSignal(object) 233 234 @property 235 def has_selection(self): 236 return self.selection_state.current_mode == 'selected' 237 238 @property 239 def is_modified(self): 240 return self.current_image is not self.original_image 241 242 # Drag 'n drop {{{ 243 244 def dragEnterEvent(self, event): 245 md = event.mimeData() 246 if dnd_has_extension(md, image_extensions()) or dnd_has_image(md): 247 event.acceptProposedAction() 248 249 def dropEvent(self, event): 250 event.setDropAction(Qt.DropAction.CopyAction) 251 md = event.mimeData() 252 253 x, y = dnd_get_image(md) 254 if x is not None: 255 # We have an image, set cover 256 event.accept() 257 if y is None: 258 # Local image 259 self.undo_stack.push(Replace(x.toImage(), _('Drop image'), self)) 260 else: 261 d = DownloadDialog(x, y, self.gui) 262 d.start_download() 263 if d.err is None: 264 with open(d.fpath, 'rb') as f: 265 img = QImage() 266 img.loadFromData(f.read()) 267 if not img.isNull(): 268 self.undo_stack.push(Replace(img, _('Drop image'), self)) 269 270 event.accept() 271 272 def dragMoveEvent(self, event): 273 event.acceptProposedAction() 274 # }}} 275 276 def __init__(self, parent=None): 277 QWidget.__init__(self, parent) 278 self.setAcceptDrops(True) 279 self.setMouseTracking(True) 280 self.setFocusPolicy(Qt.FocusPolicy.ClickFocus) 281 self.selection_state = SelectionState() 282 self.undo_stack = u = QUndoStack() 283 u.setUndoLimit(10) 284 u.canUndoChanged.connect(self.emit_undo_redo_state) 285 u.canRedoChanged.connect(self.emit_undo_redo_state) 286 287 self.original_image_data = None 288 self.is_valid = False 289 self.original_image_format = None 290 self.current_image = None 291 self.current_scaled_pixmap = None 292 self.last_canvas_size = None 293 self.target = QRectF(0, 0, 0, 0) 294 295 self.undo_action = a = self.undo_stack.createUndoAction(self, _('Undo') + ' ') 296 a.setIcon(QIcon(I('edit-undo.png'))) 297 self.redo_action = a = self.undo_stack.createRedoAction(self, _('Redo') + ' ') 298 a.setIcon(QIcon(I('edit-redo.png'))) 299 300 def load_image(self, data): 301 self.is_valid = False 302 try: 303 fmt = identify(data)[0].encode('ascii') 304 except Exception: 305 fmt = b'' 306 self.original_image_format = fmt.decode('ascii').lower() 307 self.selection_state.reset() 308 self.original_image_data = data 309 self.current_image = i = self.original_image = ( 310 QImage.fromData(data, format=fmt) if fmt else QImage.fromData(data)) 311 self.is_valid = not i.isNull() 312 self.current_scaled_pixmap = None 313 self.update() 314 self.image_changed.emit(self.current_image) 315 316 def set_image(self, qimage): 317 self.selection_state.reset() 318 self.current_scaled_pixmap = None 319 self.current_image = qimage 320 self.is_valid = not qimage.isNull() 321 self.update() 322 self.image_changed.emit(self.current_image) 323 324 def get_image_data(self, quality=90): 325 if not self.is_modified: 326 return self.original_image_data 327 fmt = self.original_image_format or 'JPEG' 328 if fmt.lower() not in {x.data().decode('utf-8') for x in QImageWriter.supportedImageFormats()}: 329 if fmt.lower() == 'gif': 330 data = image_to_data(self.current_image, fmt='PNG', png_compression_level=0) 331 from PIL import Image 332 i = Image.open(BytesIO(data)) 333 buf = BytesIO() 334 i.save(buf, 'gif') 335 return buf.getvalue() 336 else: 337 raise ValueError('Cannot save %s format images' % fmt) 338 return pixmap_to_data(self.current_image, format=fmt, quality=90) 339 340 def copy(self): 341 if not self.is_valid: 342 return 343 clipboard = QApplication.clipboard() 344 if not self.has_selection or self.selection_state.rect is None: 345 clipboard.setImage(self.current_image) 346 else: 347 trim = Trim(self) 348 clipboard.setImage(trim.after_image) 349 trim.before_image = trim.after_image = None 350 351 def paste(self): 352 clipboard = QApplication.clipboard() 353 md = clipboard.mimeData() 354 if md.hasImage(): 355 img = QImage(md.imageData()) 356 if not img.isNull(): 357 self.undo_stack.push(Replace(img, _('Paste image'), self)) 358 else: 359 error_dialog(self, _('No image'), _( 360 'No image available in the clipboard'), show=True) 361 362 def break_cycles(self): 363 self.undo_stack.clear() 364 self.original_image_data = self.current_image = self.current_scaled_pixmap = None 365 366 def emit_undo_redo_state(self): 367 self.undo_redo_state_changed.emit(self.undo_action.isEnabled(), self.redo_action.isEnabled()) 368 369 @imageop 370 def trim_image(self): 371 if self.selection_state.rect is None: 372 error_dialog(self, _('No selection'), _( 373 'No active selection, first select a region in the image, by dragging with your mouse'), show=True) 374 return False 375 self.undo_stack.push(Trim(self)) 376 return True 377 378 @imageop 379 def autotrim_image(self): 380 self.undo_stack.push(AutoTrim(self)) 381 return True 382 383 @imageop 384 def rotate_image(self): 385 self.undo_stack.push(Rotate(self)) 386 return True 387 388 @imageop 389 def resize_image(self, width, height): 390 self.undo_stack.push(Scale(width, height, self)) 391 return True 392 393 @imageop 394 def sharpen_image(self, sigma=3.0): 395 self.undo_stack.push(Sharpen(sigma, self)) 396 return True 397 398 @imageop 399 def blur_image(self, sigma=3.0): 400 self.undo_stack.push(Blur(sigma, self)) 401 return True 402 403 @imageop 404 def despeckle_image(self): 405 self.undo_stack.push(Despeckle(self)) 406 return True 407 408 @imageop 409 def normalize_image(self): 410 self.undo_stack.push(Normalize(self)) 411 return True 412 413 @imageop 414 def oilify_image(self, radius=4.0): 415 self.undo_stack.push(Oilify(radius, self)) 416 return True 417 418 # The selection rectangle {{{ 419 @property 420 def dc_size(self): 421 sr = self.selection_state.rect 422 dx = min(75, sr.width() / 4) 423 dy = min(75, sr.height() / 4) 424 return dx, dy 425 426 def get_drag_corner(self, pos): 427 dx, dy = self.dc_size 428 sr = self.selection_state.rect 429 x, y = pos.x(), pos.y() 430 hedge = 'left' if x < sr.x() + dx else 'right' if x > sr.right() - dx else None 431 vedge = 'top' if y < sr.y() + dy else 'bottom' if y > sr.bottom() - dy else None 432 return (hedge, vedge) if hedge or vedge else None 433 434 def get_drag_rect(self): 435 sr = self.selection_state.rect 436 dc = self.selection_state.drag_corner 437 if None in (sr, dc): 438 return 439 dx, dy = self.dc_size 440 if None in dc: 441 # An edge 442 if dc[0] is None: 443 top = sr.top() if dc[1] == 'top' else sr.bottom() - dy 444 ans = QRectF(sr.left() + dx, top, sr.width() - 2 * dx, dy) 445 else: 446 left = sr.left() if dc[0] == 'left' else sr.right() - dx 447 ans = QRectF(left, sr.top() + dy, dx, sr.height() - 2 * dy) 448 else: 449 # A corner 450 left = sr.left() if dc[0] == 'left' else sr.right() - dx 451 top = sr.top() if dc[1] == 'top' else sr.bottom() - dy 452 ans = QRectF(left, top, dx, dy) 453 return ans 454 455 def get_cursor(self): 456 dc = self.selection_state.drag_corner 457 if dc is None: 458 ans = Qt.CursorShape.OpenHandCursor if self.selection_state.last_drag_pos is None else Qt.CursorShape.ClosedHandCursor 459 elif None in dc: 460 ans = Qt.CursorShape.SizeVerCursor if dc[0] is None else Qt.CursorShape.SizeHorCursor 461 else: 462 ans = Qt.CursorShape.SizeBDiagCursor if dc in {('left', 'bottom'), ('right', 'top')} else Qt.CursorShape.SizeFDiagCursor 463 return ans 464 465 def update(self): 466 super().update() 467 self.selection_area_changed.emit(self.selection_state.rect) 468 469 def move_edge(self, edge, dp): 470 sr = self.selection_state.rect 471 horiz = edge in {'left', 'right'} 472 func = getattr(sr, 'set' + capitalize(edge)) 473 delta = getattr(dp, 'x' if horiz else 'y')() 474 buf = 50 475 if horiz: 476 minv = self.target.left() if edge == 'left' else sr.left() + buf 477 maxv = sr.right() - buf if edge == 'left' else self.target.right() 478 else: 479 minv = self.target.top() if edge == 'top' else sr.top() + buf 480 maxv = sr.bottom() - buf if edge == 'top' else self.target.bottom() 481 func(max(minv, min(maxv, delta + getattr(sr, edge)()))) 482 483 def move_selection_rect(self, x, y): 484 sr = self.selection_state.rect 485 half_width = sr.width() / 2.0 486 half_height = sr.height() / 2.0 487 c = sr.center() 488 nx = c.x() + x 489 ny = c.y() + y 490 minx = self.target.left() + half_width 491 maxx = self.target.right() - half_width 492 miny, maxy = self.target.top() + half_height, self.target.bottom() - half_height 493 nx = max(minx, min(maxx, nx)) 494 ny = max(miny, min(maxy, ny)) 495 sr.moveCenter(QPointF(nx, ny)) 496 497 def move_selection(self, dp): 498 dm = self.selection_state.dragging 499 if dm is None: 500 self.move_selection_rect(dp.x(), dp.y()) 501 else: 502 for edge in dm: 503 if edge is not None: 504 self.move_edge(edge, dp) 505 506 def mousePressEvent(self, ev): 507 if ev.button() == Qt.MouseButton.LeftButton and self.target.contains(ev.pos()): 508 pos = ev.pos() 509 self.selection_state.last_press_point = pos 510 if self.selection_state.current_mode is None: 511 self.selection_state.current_mode = 'select' 512 513 elif self.selection_state.current_mode == 'selected': 514 if self.selection_state.rect is not None and self.selection_state.rect.contains(pos): 515 self.selection_state.drag_corner = self.selection_state.dragging = self.get_drag_corner(pos) 516 self.selection_state.last_drag_pos = pos 517 self.setCursor(self.get_cursor()) 518 else: 519 self.selection_state.current_mode = 'select' 520 self.selection_state.rect = None 521 self.selection_state_changed.emit(False) 522 523 def mouseMoveEvent(self, ev): 524 changed = False 525 if self.selection_state.in_selection: 526 changed = True 527 self.selection_state.in_selection = False 528 self.selection_state.drag_corner = None 529 pos = ev.pos() 530 cursor = Qt.CursorShape.ArrowCursor 531 try: 532 if not self.target.contains(pos): 533 return 534 if ev.buttons() & Qt.MouseButton.LeftButton: 535 if self.selection_state.last_press_point is not None and self.selection_state.current_mode is not None: 536 if self.selection_state.current_mode == 'select': 537 self.selection_state.rect = QRectF(self.selection_state.last_press_point, pos).normalized() 538 changed = True 539 elif self.selection_state.last_drag_pos is not None: 540 self.selection_state.in_selection = True 541 self.selection_state.drag_corner = self.selection_state.dragging 542 dp = pos - self.selection_state.last_drag_pos 543 self.selection_state.last_drag_pos = pos 544 self.move_selection(dp) 545 cursor = self.get_cursor() 546 changed = True 547 else: 548 if self.selection_state.rect is None or not self.selection_state.rect.contains(pos): 549 return 550 if self.selection_state.current_mode == 'selected': 551 if self.selection_state.rect is not None and self.selection_state.rect.contains(pos): 552 self.selection_state.drag_corner = self.get_drag_corner(pos) 553 self.selection_state.in_selection = True 554 cursor = self.get_cursor() 555 changed = True 556 finally: 557 if changed: 558 self.update() 559 self.setCursor(cursor) 560 561 def mouseReleaseEvent(self, ev): 562 if ev.button() == Qt.MouseButton.LeftButton: 563 self.selection_state.dragging = self.selection_state.last_drag_pos = None 564 if self.selection_state.current_mode == 'select': 565 r = self.selection_state.rect 566 if r is None or max(r.width(), r.height()) < 3: 567 self.selection_state.reset() 568 else: 569 self.selection_state.current_mode = 'selected' 570 self.selection_state_changed.emit(self.has_selection) 571 elif self.selection_state.current_mode == 'selected' and self.selection_state.rect is not None and self.selection_state.rect.contains(ev.pos()): 572 self.setCursor(self.get_cursor()) 573 self.update() 574 575 def keyPressEvent(self, ev): 576 k = ev.key() 577 if k in (Qt.Key.Key_Left, Qt.Key.Key_Right, Qt.Key.Key_Up, Qt.Key.Key_Down) and self.selection_state.rect is not None and self.has_selection: 578 ev.accept() 579 delta = 10 if ev.modifiers() & Qt.KeyboardModifier.ShiftModifier else 1 580 x = y = 0 581 if k in (Qt.Key.Key_Left, Qt.Key.Key_Right): 582 x = delta * (-1 if k == Qt.Key.Key_Left else 1) 583 else: 584 y = delta * (-1 if k == Qt.Key.Key_Up else 1) 585 self.move_selection_rect(x, y) 586 self.update() 587 else: 588 return QWidget.keyPressEvent(self, ev) 589 # }}} 590 591 # Painting {{{ 592 @painter 593 def draw_background(self, painter): 594 painter.fillRect(self.rect(), self.BACKGROUND) 595 596 @painter 597 def draw_image_error(self, painter): 598 font = painter.font() 599 font.setPointSize(3 * font.pointSize()) 600 font.setBold(True) 601 painter.setFont(font) 602 painter.setPen(QColor(Qt.GlobalColor.black)) 603 painter.drawText(self.rect(), Qt.AlignmentFlag.AlignCenter, _('Not a valid image')) 604 605 def load_pixmap(self): 606 canvas_size = self.rect().width(), self.rect().height() 607 if self.last_canvas_size != canvas_size: 608 if self.last_canvas_size is not None and self.selection_state.rect is not None: 609 self.selection_state.reset() 610 # TODO: Migrate the selection rect 611 self.last_canvas_size = canvas_size 612 self.current_scaled_pixmap = None 613 if self.current_scaled_pixmap is None: 614 pwidth, pheight = self.last_canvas_size 615 i = self.current_image 616 width, height = i.width(), i.height() 617 scaled, width, height = fit_image(width, height, pwidth, pheight) 618 try: 619 dpr = self.devicePixelRatioF() 620 except AttributeError: 621 dpr = self.devicePixelRatio() 622 if scaled: 623 i = self.current_image.scaled(int(dpr * width), int(dpr * height), transformMode=Qt.TransformationMode.SmoothTransformation) 624 self.current_scaled_pixmap = QPixmap.fromImage(i) 625 self.current_scaled_pixmap.setDevicePixelRatio(dpr) 626 627 @painter 628 def draw_pixmap(self, painter): 629 p = self.current_scaled_pixmap 630 try: 631 dpr = self.devicePixelRatioF() 632 except AttributeError: 633 dpr = self.devicePixelRatio() 634 width, height = int(p.width()/dpr), int(p.height()/dpr) 635 pwidth, pheight = self.last_canvas_size 636 x = int(abs(pwidth - width)/2.) 637 y = int(abs(pheight - height)/2.) 638 self.target = QRectF(x, y, width, height) 639 painter.drawPixmap(self.target, p, QRectF(p.rect())) 640 641 @painter 642 def draw_selection_rect(self, painter): 643 cr, sr = self.target, self.selection_state.rect 644 painter.setPen(self.SELECT_PEN) 645 painter.setRenderHint(QPainter.RenderHint.Antialiasing, False) 646 if self.selection_state.current_mode == 'selected': 647 # Shade out areas outside the selection rect 648 for r in ( 649 QRectF(cr.topLeft(), QPointF(sr.left(), cr.bottom())), # left 650 QRectF(QPointF(sr.left(), cr.top()), sr.topRight()), # top 651 QRectF(QPointF(sr.right(), cr.top()), cr.bottomRight()), # right 652 QRectF(sr.bottomLeft(), QPointF(sr.right(), cr.bottom())), # bottom 653 ): 654 painter.fillRect(r, self.SHADE_COLOR) 655 656 dr = self.get_drag_rect() 657 if self.selection_state.in_selection and dr is not None: 658 # Draw the resize rectangle 659 painter.save() 660 painter.setCompositionMode(QPainter.CompositionMode.RasterOp_SourceAndNotDestination) 661 painter.setClipRect(sr.adjusted(1, 1, -1, -1)) 662 painter.drawRect(dr) 663 painter.restore() 664 665 # Draw the selection rectangle 666 painter.setCompositionMode(QPainter.CompositionMode.RasterOp_SourceAndNotDestination) 667 painter.drawRect(sr) 668 669 def paintEvent(self, event): 670 QWidget.paintEvent(self, event) 671 p = QPainter(self) 672 p.setRenderHints(QPainter.RenderHint.Antialiasing | QPainter.RenderHint.SmoothPixmapTransform) 673 try: 674 self.draw_background(p) 675 if self.original_image_data is None: 676 return 677 if not self.is_valid: 678 return self.draw_image_error(p) 679 self.load_pixmap() 680 self.draw_pixmap(p) 681 if self.selection_state.rect is not None: 682 self.draw_selection_rect(p) 683 finally: 684 p.end() 685 # }}} 686 687 688if __name__ == '__main__': 689 app = QApplication([]) 690 with open(sys.argv[-1], 'rb') as f: 691 data = f.read() 692 c = Canvas() 693 c.load_image(data) 694 c.show() 695 app.exec() 696