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