You are not logged in.

#1 2020-03-24 12:37:16

xerxes_
Member
Registered: 2018-04-29
Posts: 665

iview image viewer with some additional features

This is simple image viewer written in python3, all in one file with some additional features like cropping/rotating/flipping image, text or ellipse writing, pixel inspection and measuring mode, etc.

This is not my code; I found it somewhere in the net (don't remember where) and do some modifications to silence new python3.8 console output.

I think the license allow to place it here, so I do it, because I think the prog is nice. If you disagree with that, then you may remove this.

Enjoy it!

#!/usr/bin/python

# The MIT License (MIT)
#
# Copyright (c) 2014 Andrea Griffini
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

import sys, gc, os, time, math, re, subprocess

try:
    from PyQt5.Qt import *
    pyside = False
except:
    from PySide2.QtWidgets import *
    from PySide2.QtGui import *
    from PySide2.QtCore import *
    pyside = True

try:
    unicode
except:
    unicode = str

QW = QWidget

def between(x, a, b, eps=0):
    return min(a, b)-eps <= x <= max(a, b)+eps

def near(x, a, eps):
    return a-eps <= x <= a+eps

class Viewer(QW):
    def __init__(self, parent, imglist):
        QW.__init__(self, parent)

        bg = QImage(32, 32, QImage.Format_ARGB32)
        dc = QPainter(bg)
        dc.fillRect(0, 0, 32, 32, QColor(96, 96, 96))
        dc.fillRect(0, 0, 16, 16, QColor(128, 128, 128))
        dc.fillRect(16, 16, 16, 16, QColor(128, 128, 128))
        dc = None
        self.bbrush = QBrush(bg)

        self.setFocusPolicy(Qt.StrongFocus)
        self.setFocus()
        self.imglist = imglist[:]
        self.setMouseTracking(True)
        self.fullscreen = False
        self.titleOverlay = False
        self.timer = QTimer(self)
        self.timer.timeout.connect(self.checkmtime)
        self.timer.start(250)
        self.pos = None
        self.tracking = None
        self.tools = []
        self.vector_overlay = []
        self.load(0)

    def load(self, index):
        img = QImage(self.imglist[index])
        if img.width() > 0:
            self.vector_overlay = []
            try:
                #
                # .vo file = vector overlay data, each line is
                #    <command>,<color>,<parm1>,<parm2>,...
                #
                # commands are:
                #    "L" = polyline (x1, y1, x2, y2, ...)
                #    "P" = filled polygon (x1, y1, x2, y2, ...)
                #    "C" = circle (r, cx1, cy1, cx2, cy2, ...)
                #    "x" = cross (x1, y1, x2, y2, ...)
                #    "+" = cross (x1, y1, x2, y2, ...)
                #
                for L in open(self.imglist[index] + ".vo"):
                    parts = L.strip().split(",")
                    if len(parts) > 2:
                        self.vector_overlay.append(parts[:2] + list(map(float, parts[2:])))
            except:
                pass
            self.imgmtime = os.stat(self.imglist[index]).st_mtime
        else:
            self.imgmtime = None
        self.imglist[:] = self.imglist[index:] + self.imglist[:index]
        self.img = img
        self.scaled = None
        self.parent().setWindowTitle(self.mkTitle())
        self.update()
        gc.collect()

    def checkmtime(self):
        try:
            imgmtime = os.stat(self.imglist[0]).st_mtime
        except:
            imgmtime = None
        if imgmtime != self.imgmtime:
            ow = self.img.width()
            oh = self.img.height()
            self.load(0)
            if self.img.width() != ow or self.img.height() != oh:
                self.zoomAll()

    def resizeEvent(self, e):
        self.zoomAll()
        QW.resizeEvent(self, e)

    def zoomAll(self):
        self.scale = min(float(self.width()) / max(1, self.img.width()),
                         float(self.height()) / max(1, self.img.height()))
        self.tx = (self.width() - self.img.width()*self.scale) / 2
        self.ty = (self.height() - self.img.height()*self.scale) / 2
        self.update()

    def keyPressEvent(self, e):
        for t in self.tools:
            if t.key(e):
                self.update()
                return
        if e.key() == Qt.Key_Right:
            w, h = self.img.width(), self.img.height()
            self.load(min(1, len(self.imglist) - 1))
            if (w, h) != (self.img.width(), self.img.height()):
                self.zoomAll()
        elif e.key() == Qt.Key_Left:
            w, h = self.img.width(), self.img.height()
            self.load(len(self.imglist) - 1)
            if (w, h) != (self.img.width(), self.img.height()):
                self.zoomAll()
        elif e.key() == Qt.Key_1:
            self.setZoom(1)
        elif e.key() == Qt.Key_2:
            self.setZoom(2)
        elif e.key() == Qt.Key_3:
            self.setZoom(3)
        elif e.key() == Qt.Key_4:
            self.setZoom(4)
        elif e.key() in (Qt.Key_Home, Qt.Key_0):
            self.zoomAll()
        elif e.key() in (Qt.Key_Escape, Qt.Key_Q):
            self.parent().deleteLater()
        elif e.key() == Qt.Key_U:
            w, h = self.img.width(), self.img.height()
            self.load(0)
            if (w, h) != (self.img.width(), self.img.height()):
                self.zoomAll()
        elif e.key() in (Qt.Key_F, Qt.Key_Return, Qt.Key_Enter):
            self.fullscreen = not self.fullscreen
            if self.fullscreen:
                self.parent().showFullScreen()
            else:
                self.parent().showNormal()
        elif e.key() == Qt.Key_R:
            img = QImage(self.img.height(), self.img.width(), QImage.Format_ARGB32)
            img.fill(0)
            dc = QPainter(img)
            dc.rotate(90)
            dc.drawImage(0, -self.img.height(), self.img)
            dc = None
            self.img = img
            self.scaled = None
            self.zoomAll()
            gc.collect()
        elif e.key() == Qt.Key_H:
            self.img = self.img.mirrored(True, False)
            self.scaled = None
            self.update()
            gc.collect()
        elif e.key() == Qt.Key_V:
            self.img = self.img.mirrored(False, True)
            self.scaled = None
            self.update()
            gc.collect()
        elif e.key() == Qt.Key_C:
            self.tools = [CropTool(self)]
        elif e.key() == Qt.Key_E:
            self.tools = [EllipseTool(self)]
        elif e.key() == Qt.Key_P:
            self.tools = [PixColorTool(self)]
        elif e.key() == Qt.Key_A:
            self.tools = [ArrowTool(self)]
        elif e.key() == Qt.Key_M:
            self.tools = [MeasuringTool(self)]
        elif e.key() == Qt.Key_Y:
            self.to32bpp()
            b = self.img.bits()
            if not pyside:
                b.setsize(self.img.height() * self.img.bytesPerLine())
                b.setwriteable(True)
            for i in range(0, self.img.height()*self.img.bytesPerLine(), 4):
                b[i] = b[i+2] = b[i+1]
            self.scaled = None
        elif e.key() == Qt.Key_T:
            self.tools = [TextTool(self)]
        elif e.key() == Qt.Key_S:
            fname = str(QFileDialog.getSaveFileName(self, "Save image as", self.imglist[0])[0])
            if fname != "":
                supported_extensions = [x.data().decode() for x in QImageWriter.supportedImageFormats()]
                if re.match("^.*\\.(" + "|".join(supported_extensions) + ")$", fname, re.IGNORECASE):
                    if self.img.save(fname):
                        if fname not in self.imglist:
                            self.imglist.insert(0, fname)
                            self.load(0)
                    else:
                        QMessageBox.critical(self, "IView: Error saving image file",
                                             "There was a problem saving the image file.")
                elif fname[-4:].lower() == ".pgm":
                    # Hand-made PGM writer
                    try:
                        self.to32bpp()
                        data = self.img.bits().asstring(self.img.height() * self.img.bytesPerLine())
                        f = open(fname, "wb")
                        f.write(("P5\n%i %i 255\n" % (self.img.width(), self.img.height()))
                                .encode("ascii"))
                        for y in range(self.img.height()):
                            f.write(data[y*self.img.width()*4+1:(y+1)*self.img.width()*4+1:4])
                        f.close()
                        if fname not in self.imglist:
                            self.imglist.insert(0, fname)
                            self.load(0)
                    except Exception:
                        QMessageBox.critical(self, "IView: Error saving image file",
                                             "There was a problem saving the image file.")
                else:
                    QMessageBox.critical(self, "IView: Unable to save image file",
                                         "The requested image format is not supported, supported extensions are "+
                                         ", ".join(supported_extensions))
        elif e.key() == Qt.Key_X:
            subprocess.Popen([os.getenv("PIXEDITOR") or "gimp", self.imglist[0]])
        elif e.key() == Qt.Key_Delete:
            if QMessageBox.question(self,
                                    "Image deletion",
                                    "Are you sure you want to delete this image?",
                                    QMessageBox.Yes|QMessageBox.No) == QMessageBox.Yes:
                try:
                    os.unlink(self.imglist[0])
                except OSError:
                    QMessageBox.information(self, "IView",
                                            "Error deleting image file")
                    return
                self.scaled = None
                self.imglist.pop(0)
                if len(self.imglist) == 0:
                    sys.exit(0)
                self.load(0)
                self.zoomAll()
        elif e.key() == Qt.Key_F2:
            name = self.imglist[0]
            d = QDialog(self)
            d.setWindowTitle("Rename image")
            vb = QVBoxLayout(d)
            l = QLabel("New name"); vb.addWidget(l)
            sle = QLineEdit(d); vb.addWidget(sle)
            hb = QHBoxLayout(); vb.addLayout(hb);
            hb.addWidget(QWidget(d))
            ok = QPushButton("OK", d); hb.addWidget(ok)
            ok.setFixedWidth(80)
            cancel = QPushButton("Cancel", d); hb.addWidget(cancel)
            cancel.setFixedWidth(80)
            hb.addWidget(QWidget(d))
            sle.setText(name)
            basename_start = 1+name.rindex("/") if "/" in name else 0
            if "." in name[basename_start:]:
                sle.setSelection(basename_start, name[basename_start:].rindex("."))
            sle.setFocus()
            width = sle.fontMetrics().boundingRect(name).width()
            sle.setMinimumWidth(width*9//8)
            cancel.clicked.connect(d.reject)
            ok.clicked.connect(d.accept)
            if d.exec_() == QDialog.Accepted:
                try:
                    s = sle.text()
                    os.rename(self.imglist[0], unicode(s))
                    self.imglist[0] = unicode(s)
                    self.load(0)
                except Exception:
                    QMessageBox.critical(self, "IView: Unable to rename the file",
                                         "There was a problem renaming the file")
        elif e.key() == Qt.Key_O:
            self.titleOverlay = not self.titleOverlay
        elif e.key() == Qt.Key_F1:
            QMessageBox.information(self, "IView",
                                    "<center><h1>IView</h1>A simple image viewer<br>"
                                    "by Andrea \"6502\" Griffini</center>"
                                    "<ul>"
                                    "  <li> <b>F1</b> This help message</li>"
                                    "  <li> <b>Mouse drag</b> Pan</li>"
                                    "  <li> <b>Mouse wheel</b> Zoom</li>"
                                    "  <li> <b>Left/Right</b> Previous/next image</li>"
                                    "  <li> <b>Home</b> Zoom fit</li>"
                                    "  <li> <b>1/2/3/4</b> Display resolution to Nx</li>"
                                    "  <li> <b>F or Enter</b> Fullscreen mode</li>"
                                    "  <li> <b>O</b> Toggle file name overlay in fullscreen mode</li>"
                                    "  <li> <b>A</b> Arrow markup mode</li>"
                                    "  <li> <b>P</b> Pixel inspection mode</li>"
                                    "  <li> <b>M</b> Measure mode</li>"
                                    "  <li> <b>E</b> Ellipse markup mode</li>"
                                    "  <li> <b>T</b> Text markup mode</li>"
                                    "  <li> <b>C</b> Cropping/yanking mode</li>"
                                    "  <li> <b>Y</b> Convert to grayscale</li>"
                                    "  <li> <b>R</b> Rotate image 90 degrees</li>"
                                    "  <li> <b>H</b> Horizontal flip</li>"
                                    "  <li> <b>V</b> Vertical flip</li>"
                                    "  <li> <b>U</b> Reload original image</li>"
                                    "  <li> <b>S</b> Save modified image</li>"
                                    "  <li> <b>F2</b> Rename current file</li>"
                                    "  <li> <b>Del</b> Delete current file</li>"
                                    "  <li> <b>X</b> Run external editor</li>"
                                    "</ul>")
        self.update()

    def sizeHint(self):
        return QSize(900, 700)

    def setZoom(self, sf):
        if self.pos:
            mx, my = self.pos[0] * self.scale + self.tx, self.pos[1] * self.scale + self.ty
            self.scale = sf
            self.tx = mx - self.pos[0] * self.scale
            self.ty = my - self.pos[1] * self.scale
        else:
            mx, my = self.width()/2, self.height()/2
            lx, ly = (mx - self.tx)/self.scale, (my - self.ty)/self.scale
            self.scale = sf
            self.tx = mx - lx * self.scale
            self.ty = my - ly * self.scale
        self.update()

    def wheelEvent(self, e):
        k = 1.01 if (int(e.modifiers()) & Qt.ShiftModifier) != 0 else 1.2
        self.pos = (e.x() - self.tx) / self.scale, (e.y() - self.ty) / self.scale
        sf = self.scale
        d = int(e.angleDelta().y() / 120.)
        while d > 0:
            sf *= k
            d -= 1
        while d < 0:
            sf /= k
            d += 1
        if sf < 0.01:
            sf = 0.01
        if sf > 10000:
            sf = 10000
        self.setZoom(sf)

    def paintEvent(self, e):
        dc = QPainter(self)
        w, h = self.width(), self.height()
        dc.fillRect(0, 0, w, h, QBrush(QColor(64, 64, 64)))

        dc.fillRect(int(self.tx+1), int(self.ty+1), int(self.img.width()*self.scale-2), int(self.img.height()*self.scale-2), self.bbrush)
        if self.scale > 1:
            xf = QTransform().translate(self.tx, self.ty).scale(self.scale, self.scale)
            dc.drawImage(QRectF(self.rect()), self.img, xf.inverted()[0].mapRect(QRectF(self.rect())))
        else:
            if self.scaled is None or self.scaled[0] != self.scale:
                sw = int(self.img.width() * self.scale)
                sh = int(self.img.height() * self.scale)
                simg = self.img.scaled(QSize(sw, sh), Qt.IgnoreAspectRatio, Qt.SmoothTransformation)
                self.scaled = [self.scale, simg]
            dc.drawImage(int(self.tx), int(self.ty), self.scaled[1])
        for L in self.vector_overlay:
            if L[0] == 'L':
                dc.setPen(QPen(QColor(L[1])))
                pts = []
                for i in range(2, len(L)-1, 2):
                    pts.append(QPointF(L[i]*self.scale + self.tx, L[i+1]*self.scale + self.ty))
                dc.drawPolyline(QPolygonF(pts))
            elif L[0] == 'P':
                dc.setBrush(QBrush(QColor(L[1])))
                dc.setPen(Qt.NoPen)
                pts = []
                for i in range(2, len(L)-1, 2):
                    pts.append(QPointF(L[i]*self.scale + self.tx, L[i+1]*self.scale + self.ty))
                dc.drawPolygon(QPolygonF(pts))
            elif L[0] == 'C':
                dc.setBrush(QBrush())
                dc.setPen(QPen(QColor(L[1])))
                r = L[2] * self.scale
                pts = []
                for i in range(3, len(L)-1, 2):
                    dc.drawEllipse(QRectF(L[i]*self.scale + self.tx - r,
                                          L[i+1]*self.scale + self.ty - r,
                                          r*2, r*2))
            elif L[0] == 'x' or L[0] == '+':
                dc.setPen(QPen(QColor(L[1])))
                for i in range(2, len(L)-1, 2):
                    x = L[i]*self.scale + self.tx
                    y = L[i+1]*self.scale + self.ty
                    if L[0] == 'x':
                        dc.drawLine(QPointF(x-10, y-10), QPointF(x+10, y+10))
                        dc.drawLine(QPointF(x+10, y-10), QPointF(x-10, y+10))
                    else:
                        dc.drawLine(QPointF(x-14, y), QPointF(x+14, y))
                        dc.drawLine(QPointF(x, y-14), QPointF(x, y+14))
        y = 16
        dc.setFont(QFont("sans-serif", 11))

        if len(self.tools)==0 and self.fullscreen and self.titleOverlay:
            self.statusText(dc, self.mkTitle())

        for t in self.tools:
            t.draw(dc)

    def mkTitle(self):
        if self.img.isNull():
            name = self.imglist[0] + " (unable to load as image)"
        else:
            name = self.imglist[0] + " (%i x %i x %ibpp = %0.2f MB)" % (
                self.img.width(),
                self.img.height(),
                self.img.depth(),
                self.img.height() * self.img.bytesPerLine() / (1024.0*1024.0))
        if "/" in name:
            name = name[name.rindex("/")+1:]
        return name

    def mousePressEvent(self, e):
        self.pos = ((e.x() - self.tx) / self.scale,
                    (e.y() - self.ty) / self.scale)
        for t in self.tools:
            self.tracking = t.hit(e.x(), e.y(), e.button())
            if self.tracking:
                break
        else:
            cur = [e.x(), e.y()]
            def pan(mx, my):
                if mx is not None:
                    self.tx += mx - cur[0]
                    self.ty += my - cur[1]
                    cur[0] = mx
                    cur[1] = my
            self.tracking = pan

    def mouseMoveEvent(self, e):
        self.pos = ((e.x() - self.tx) / self.scale,
                    (e.y() - self.ty) / self.scale)
        if self.tracking:
            self.tracking(e.x(), e.y())
            self.update()

    def mouseReleaseEvent(self, e):
        if self.tracking:
            self.tracking(None, None)
            self.tracking = None

    def map(self, x, y):
        return self.tx + x*self.scale, self.ty + y*self.scale

    def revmap(self, x, y):
        return (x - self.tx)/self.scale, (y - self.ty)/self.scale

    def to32bpp(self):
        if self.img.format() != QImage.Format_ARGB32:
            self.img = self.img.convertToFormat(QImage.Format_ARGB32)
            self.scaled = None
            self.update()

    def statusText(self, dc, msg):
        dc.setPen(QPen(QColor(0, 0, 0)))
        dc.drawText(4, 20, msg )
        dc.drawText(7, 20, msg )
        dc.drawText(5, 19, msg )
        dc.drawText(5, 21, msg )
        dc.setPen(QPen(QColor(0, 255, 0)))
        dc.drawText(5, 20, msg )

class Tool(object):
    pen = 4
    color = 0
    colors = [QColor(255, 0, 0),
              QColor(0, 255, 0),
              QColor(0, 0, 255)]

    def __init__(self, iview):
        self.iview = iview
        self.font = QFont("sans-serif")
        self.font.setPixelSize(self.pen * 8 + 11)

    def key(self, e):
        if e.key() == Qt.Key_R:
            Tool.color = 0
            self.iview.update()
            return True
        if e.key() == Qt.Key_G:
            Tool.color = 1
            self.iview.update()
            return True
        if e.key() == Qt.Key_B:
            Tool.color = 2
            self.iview.update()
            return True
        if e.key() >= Qt.Key_1 and e.key() <= Qt.Key_9:
            Tool.pen = (e.key() - int(Qt.Key_1))*2
            self.font.setPixelSize(self.pen * 8 + 11)
            self.iview.update()
            return True
        if e.key() == Qt.Key_Escape:
            self.iview.tools.remove(self)
            return True

    def text(self, dc, msg):
        return self.iview.statusText(dc, msg)

class RectTool(Tool):
    def __init__(self, iview):
        Tool.__init__(self, iview)
        self.geo = [0, 0, 0, 0]

    def rect(self):
        x0, y0, x1, y1 = self.geo
        x0, y0 = map(int, self.iview.map(self.geo[0], self.geo[1]))
        x1, y1 = map(int, self.iview.map(self.geo[2], self.geo[3]))
        if x0 > x1: x0, x1 = x1, x0
        if y0 > y1: y0, y1 = y1, y0
        return x0, y0, x1, y1

    def draw(self, dc):
        x0, y0, x1, y1 = self.rect()
        dc.setBrush(QBrush())
        dc.setPen(QPen(QColor(0, 0, 0), 4))
        dc.drawRect(QRectF(x0, y0, x1-x0, y1-y0))
        dc.setPen(QPen(QColor(255, 255, 255), 2, Qt.DashLine))
        dc.drawRect(QRectF(x0, y0, x1-x0, y1-y0))
        dc.setPen(QPen(QColor(0, 0, 0)))
        dc.setBrush(QColor(255, 0, 0))
        for x, y in ((x0, y0), (x1, y0), (x0, y1), (x1, y1)):
            dc.drawEllipse(QRectF(x-4, y-4, 8, 8))

    def hit(self, mx, my, button):
        if button == Qt.MiddleButton and app.keyboardModifiers() == Qt.ShiftModifier:
            print(self.geo)
        elif button == Qt.LeftButton:
            x0, y0, x1, y1 = self.geo
            x0, y0 = map(int, self.iview.map(self.geo[0], self.geo[1]))
            x1, y1 = map(int, self.iview.map(self.geo[2], self.geo[3]))
            f = 0
            if near(mx, x0, 8) and between(my, y0, y1, 8): f |= 1
            if near(my, y0, 8) and between(mx, x0, x1, 8): f |= 2
            if near(mx, x1, 8) and between(my, y0, y1, 8): f |= 4
            if near(my, y1, 8) and between(mx, x0, x1, 8): f |= 8
            if f == 0 and between(mx, x0, x1) and between(my, y0, y1):
                f = 15
            else:
                if (f & 5) == 5: f ^= 4
                if (f & 10) == 10: f ^= 8
            if f == 0:
                xx, yy = map(int, self.iview.revmap(mx, my))
                self.geo = [xx, yy, xx, yy]
                f = 4+8

            if f:
                cur = list(map(int, self.iview.revmap(mx, my)))
                def tracking(x, y):
                    if x is not None:
                        x, y = map(int, self.iview.revmap(x, y))
                        dx, dy = x - cur[0], y - cur[1]
                        cur[0], cur[1] = x, y
                        if f & 1: self.geo[0] += dx
                        if f & 2: self.geo[1] += dy
                        if f & 4: self.geo[2] += dx
                        if f & 8: self.geo[3] += dy
                        self.geo[0] = max(0, min(self.geo[0], self.iview.img.width()))
                        self.geo[1] = max(0, min(self.geo[1], self.iview.img.height()))
                        self.geo[2] = max(0, min(self.geo[2], self.iview.img.width()))
                        self.geo[3] = max(0, min(self.geo[3], self.iview.img.height()))
                return tracking

class CropTool(RectTool):
    def __init__(self, iview):
        RectTool.__init__(self, iview)

    def draw(self, dc):
        x0, y0, x1, y1 = self.rect()
        dark = QBrush(QColor(0, 0, 0, 128))
        w, h = self.iview.width(), self.iview.height()
        if y0 > 0: dc.fillRect(0, 0, w, y0, dark)
        if x0 > 0: dc.fillRect(0, y0, x0, y1-y0, dark)
        if x1 < w: dc.fillRect(x1, y0, w-x1, y1-y0, dark)
        if y1 < h: dc.fillRect(0, y1, w, h-y1, dark)
        RectTool.draw(self, dc)
        self.text(dc, "Crop: (%i, %i) - (%i, %i) = %i x %i (A to select all, Y to copy)" %
                      (self.geo[0], self.geo[1],
                       self.geo[2], self.geo[3],
                       abs(self.geo[2]-self.geo[0]),
                       abs(self.geo[3]-self.geo[1])))

    def key(self, e):
        if e.key() in (Qt.Key_Return, Qt.Key_C, Qt.Key_Enter, Qt.Key_Y):
            x0, y0, x1, y1 = self.geo
            if x0 > x1: x0, x1 = x1, x0
            if y0 > y1: y0, y1 = y1, y0
            x0, y0 = max(0, x0), max(0, y0)
            x1, y1 = min(self.iview.img.width(), x1), min(self.iview.img.height(), y1)
            if x0 < x1 and y0 < y1:
                cropped = self.iview.img.copy(x0, y0, x1-x0, y1-y0)
                if e.key() == Qt.Key_Y:
                    QApplication.clipboard().setImage(cropped)
                else:
                    self.iview.img = cropped
                    self.iview.scaled = None
                    self.iview.zoomAll()
            self.iview.tools.remove(self)
            return True
        elif e.key() in (Qt.Key_A, ):
            self.geo = [0, 0, self.iview.img.width(), self.iview.img.height()]
            return True
        else:
            return Tool.key(self, e)

class EllipseTool(RectTool):
    def __init__(self, iview):
        RectTool.__init__(self, iview)

    def draw(self, dc):
        x0, y0, x1, y1 = self.rect()
        dc.setPen(QPen(self.colors[self.color], self.pen*self.iview.scale))
        dc.setBrush(QBrush())
        dc.drawEllipse(x0, y0, x1-x0, y1-y0)
        dc.setPen(QPen(QColor(0, 0, 0)))
        dc.setBrush(QColor(255, 0, 0))
        for x, y in (((x0+x1)/2, y0), ((x0+x1)/2, y1),
                     (x0, (y0+y1)/2), (x1, (y0+y1)/2),
                     (x0, y0), (x1, y0), (x0, y1), (x1, y1)):
            dc.drawEllipse(QRectF(x-4, y-4, 8, 8))
        self.text(dc, "Ellipse drawing mode: 1-9=thickness, r/g/b=color")

    def key(self, e):
        if e.key() in (Qt.Key_Return, Qt.Key_E, Qt.Key_Enter):
            x0, y0, x1, y1 = self.geo
            if x0 > x1: x0, x1 = x1, x0
            if y0 > y1: y0, y1 = y1, y0
            x0, y0 = max(0, x0), max(0, y0)
            x1, y1 = min(self.iview.img.width(), x1), min(self.iview.img.height(), y1)
            if x0 < x1 and y0 < y1:
                self.iview.to32bpp()
                dc = QPainter(self.iview.img)
                dc.setPen(QPen(self.colors[self.color], self.pen))
                dc.setBrush(QBrush())
                dc.drawEllipse(x0, y0, x1-x0, y1-y0)
                dc = None
                self.iview.scaled = None
            self.iview.tools.remove(self)
            return True
        else:
            return Tool.key(self, e)

class PixColorTool(Tool):
    def __init__(self, iview):
        Tool.__init__(self, iview)
        self.p = None

    def draw(self, dc):
        if self.p is None:
            self.text(dc, "Pixel inspection; click for color/position")
        else:
            x, y = self.p
            col = self.iview.img.pixel(x, y)
            a = (col >> 24) & 255
            r = (col >> 16) & 255
            g = (col >> 8) & 255
            b = (col >> 0) & 255
            c0 = QBrush(QColor(0, 0, 0))
            c1 = QBrush(QColor(255, 255, 255))

            x0, y0 = self.iview.map(x, y)
            x1, y1 = self.iview.map(x+1, y+1)
            h = (y1 - y0) * 10
            s = (y1 - y0) * 2
            dc.fillRect(int(x0-1), int(y0-h-s-1), int(x1-x0+2), int(h+2), c0)
            dc.fillRect(int(x0-1), int(y1+s-1), int(x1-x0+2), int(h+2), c0)
            dc.fillRect(int(x0-h-s-1), int(y0-1), int(h+2), int(y1-y0+2), c0)
            dc.fillRect(int(x1+s-1), int(y0-1), int(h+2), int(y1-y0+2), c0)
            dc.fillRect(int(x0), int(y0-h-s), int(x1-x0), int(h), c1)
            dc.fillRect(int(x0), int(y1+s), int(x1-x0), int(h), c1)
            dc.fillRect(int(x0-h-s), int(y0), int(h), int(y1-y0), c1)
            dc.fillRect(int(x1+s), int(y0), int(h), int(y1-y0), c1)
            self.text(dc, "Pixel(x=%i, y=%i): rgb(%i, %i, %i), #%02x%02x%02x, alpha=%i" % (x, y, r, g, b, r, g, b, a))

    def hit(self, mx, my, button):
        if button == Qt.LeftButton:
            def proc(x, y):
                if 0 <= x < self.iview.img.width() and 0 <= y < self.iview.img.height():
                    self.p = (x, y)
                    self.iview.update()
            proc(*map(int, self.iview.revmap(mx, my)))
            def tracking(x, y):
                if x is not None:
                    proc(*map(int, self.iview.revmap(x, y)))
            return tracking

class ArrowTool(Tool):
    k = 8

    def __init__(self, iview):
        Tool.__init__(self, iview)
        self.pts = [0, 0, 0, 0]

    def draw(self, dc):
        x0, y0 = map(int, self.iview.map(*self.pts[:2]))
        x1, y1 = map(int, self.iview.map(*self.pts[2:]))
        dx, dy = (x1 - x0)//self.k, (y1 - y0)//self.k
        nx, ny = (y1 - y0)//self.k//2, (x0 - x1)//self.k//2
        dc.setPen(QPen(self.colors[self.color], self.pen*self.iview.scale,
                       Qt.SolidLine, Qt.RoundCap))
        dc.setBrush(QBrush())
        dc.drawLine(x0, y0, x1, y1)
        dc.drawLine(x1-dx-nx, y1-dy-ny, x1, y1)
        dc.drawLine(x1-dx+nx, y1-dy+ny, x1, y1)
        self.text(dc, "Arrow drawing mode, 1-9=thickness, r/g/b=color, +/- changes head")

    def hit(self, mx, my, button):
        if button == Qt.LeftButton:
            x0, y0 = map(int, self.iview.map(*self.pts[:2]))
            x1, y1 = map(int, self.iview.map(*self.pts[2:]))
            i = -1
            if (mx - x0)**2 + (my - y0)**2 < 32**2:
                i = 0
            elif (mx - x1)**2 + (my - y1)**2 < 32**2:
                i = 2
            else:
                xx, yy = self.iview.revmap(mx, my)
                self.pts = [xx, yy, xx, yy]
                i = 2
            def tracking(x, y):
                if x is not None:
                    self.pts[i:i+2] = list(map(int, self.iview.revmap(x, y)))
            return tracking

    def key(self, e):
        if e.key() == Qt.Key_Minus:
            if ArrowTool.k < 100: ArrowTool.k += 1
        if e.key() == Qt.Key_Plus:
            if ArrowTool.k > 4: ArrowTool.k -= 1
        if e.key() in (Qt.Key_Return, Qt.Key_A, Qt.Key_Enter):
            self.iview.to32bpp()
            x0, y0, x1, y1 = map(int, self.pts)
            dx, dy = (x1 - x0)//self.k, (y1 - y0)//self.k
            nx, ny = (y1 - y0)//self.k//2, (x0 - x1)//self.k//2
            dc = QPainter(self.iview.img)
            dc.setPen(QPen(self.colors[self.color], self.pen,
                           Qt.SolidLine, Qt.RoundCap))
            dc.setBrush(QBrush())
            dc.drawLine(x0, y0, x1, y1)
            dc.drawLine(x1-dx-nx, y1-dy-ny, x1, y1)
            dc.drawLine(x1-dx+nx, y1-dy+ny, x1, y1)
            dc = None
            self.iview.scaled = None
            self.iview.tools.remove(self)
            return True
        return Tool.key(self, e)

class MeasuringTool(Tool):
    k = 8

    def __init__(self, iview):
        Tool.__init__(self, iview)
        self.pts = [0, 0, 0, 0]

    def draw(self, dc):
        x0, y0 = map(int, self.iview.map(*self.pts[:2]))
        x1, y1 = map(int, self.iview.map(*self.pts[2:]))
        dx, dy = (x1 - x0)//self.k, (y1 - y0)//self.k
        nx, ny = (y1 - y0)//self.k//2, (x0 - x1)//self.k//2
        dc.setPen(QPen(self.colors[self.color]))
        dc.setBrush(QBrush())
        dc.drawLine(x0, y0, x1, y1)
        dc.drawLine(x1-dx-nx, y1-dy-ny, x1, y1)
        dc.drawLine(x1-dx+nx, y1-dy+ny, x1, y1)
        dc.drawLine(x0+dx-nx, y0+dy-ny, x0, y0)
        dc.drawLine(x0+dx+nx, y0+dy+ny, x0, y0)
        dc.drawLine(x0+nx, y0+ny, x0-nx, y0-ny)
        dc.drawLine(x1+nx, y1+ny, x1-nx, y1-ny)
        x0, y0, x1, y1 = self.pts
        dx, dy = x1 - x0, y1 - y0
        L = (dx**2 + dy**2)**0.5
        angle = 0 if L == 0 else math.atan2(dy, dx)*180/math.pi
        self.text(dc, "Measuring mode: (%i,%i)-(%i,%i) : %ix%i = %0.3fpx, %0.3f degrees" % (
                x0, y0, x1, y1, dx, dy, L, angle))

    def hit(self, mx, my, button):
        if button == Qt.LeftButton:
            x0, y0 = map(int, self.iview.map(*self.pts[:2]))
            x1, y1 = map(int, self.iview.map(*self.pts[2:]))
            i = -1
            if (mx - x0)**2 + (my - y0)**2 < 8**2:
                i = 0
            elif (mx - x1)**2 + (my - y1)**2 < 8**2:
                i = 2
            else:
                xx, yy = map(int, self.iview.revmap(mx, my))
                self.pts = [xx, yy, xx, yy]
                i = 2
            def tracking(x, y):
                if x is not None:
                    self.pts[i:i+2] = list(map(int, self.iview.revmap(x, y)))
                    if app.keyboardModifiers() == Qt.ShiftModifier:
                        if (abs(self.pts[i] - self.pts[0+2-i]) <
                            abs(self.pts[i+1] - self.pts[1+3-(i+1)])):
                            self.pts[i] = self.pts[0+2-i]
                        else:
                            self.pts[i+1] = self.pts[1+3-(i+1)]
            return tracking

    def key(self, e):
        if e.key() == Qt.Key_Minus:
            if MeasuringTool.k < 100: MeasuringTool.k += 1
        if e.key() == Qt.Key_Plus:
            if MeasuringTool.k > 4: MeasuringTool.k -= 1
        return Tool.key(self, e)

class TextTool(Tool):
    def __init__(self, iview):
        Tool.__init__(self, iview)
        self.text = ""
        x0, y0 = map(int, self.iview.revmap(0, 0))
        x1, y1 = map(int, self.iview.revmap(self.iview.width(), self.iview.height()))
        self.pos = [(x0 + x1)//2, (y0 + y1)//2]
        self.blink = int(time.time() * 2) + 1
        QTimer.singleShot(250, lambda : self.iview.update())

    def draw(self, dc):
        x, y = self.pos
        dc.save()
        dc.translate(self.iview.tx, self.iview.ty)
        dc.scale(self.iview.scale, self.iview.scale)
        dc.setFont(self.font)
        dc.setPen(QPen(self.colors[self.color]))
        caret = (int(time.time() * 2) - self.blink) & 1
        dc.drawText(x, y, self.text + "|" if caret else self.text)
        dc.restore()
        Tool.text(self, dc, "Text tool: ctrl-1...9=Font size, ctrl-R/G/B color, ENTER=Accept")
        QTimer.singleShot(250, lambda : self.iview.update())

    def hit(self, mx, my, button):
        if button == Qt.LeftButton:
            cur = list(map(int, self.iview.revmap(mx, my)))
            def tracking(mx, my):
                if mx is not None:
                    x, y = map(int, self.iview.revmap(mx, my))
                    self.pos[0] += x - cur[0]
                    self.pos[1] += y - cur[1]
                    cur[0] = x
                    cur[1] = y
            return tracking

    def key(self, e):
        self.blink = int(time.time() * 2) + 1
        if e.key() == Qt.Key_Backspace:
            self.text = self.text[:-1]
            return True
        elif e.key() == Qt.Key_Escape:
            return Tool.key(self, e)
        elif e.key() in (Qt.Key_Return, Qt.Key_Enter):
            self.iview.to32bpp()
            x, y = self.pos
            dc = QPainter(self.iview.img)
            dc.setFont(self.font)
            dc.setPen(QPen(self.colors[self.color]))
            dc.drawText(x, y, self.text)
            dc = None
            self.iview.scaled = None
            self.iview.tools.remove(self)
            return True
        elif e.text() and e.modifiers() != Qt.ControlModifier:
            self.text += unicode(e.text())
            return True
        return Tool.key(self, e)

class MyDialog(QDialog):
    def __init__(self, parent, imglist):
        QDialog.__init__(self, parent, Qt.Window)
        self.ws = Viewer(self, imglist)
        L = QVBoxLayout(self)
        L.setContentsMargins(0, 0, 0, 0)
        L.addWidget(self.ws)
        self.setModal(True)
        self.show()

uargv = sys.argv
if len(uargv) == 1:
    print("Missing filename")
    sys.exit(1)

app = QApplication(sys.argv)

# override the excepthook to actually quit on Ctrl-C
def ctrlc_excepthook(type, value, tback):
    if isinstance(value, KeyboardInterrupt):
        # if we got a Ctrl-C try to quit gracefully
        app.quit()
    else:
        # otherwise call the default hook
        sys.__excepthook__(type, value, tback)

sys.excepthook = ctrlc_excepthook

if sys.version_info[0]<3:
    # if we are on Python 2, let Qt deal with the encoding madness
    # (on Python 3 sys.argv is already a list of unicode strings)
    uargv = [unicode(x) for x in app.arguments()]

if len(uargv) in (2, 3) and uargv[1] == "--screenshot":
    screen = QGuiApplication.primaryScreen()
    img = screen.grabWindow(0);
    fname = "iview-screenshot.png" if len(uargv) == 2 else uargv[2]
    img.save(fname)
    uargv[1:] = [fname]

class SortWrapper:
    def __init__(self, x):
        self.x = x

    def __lt__(self, other):
        if type(self.x) is type(other.x):
            return self.x < other.x
        else:
            return id(type(self.x)) < id(type(other.x))

    def __eq__(self, other):
        return self.x == other.x

def smartSort(s):
    return tuple(SortWrapper(int(x)) if x[0] in "0123456789" else SortWrapper(x)
                 for x in re.findall("[0-9]+|[^0-9]+", s.upper()))

filelist = uargv[1:]
if len(filelist) == 1 and filelist[0][:8] == "file:///":
    filelist[0] = filelist[0][7:]
if len(filelist) == 1:
    slash = filelist[0].rindex("/") if "/" in filelist[0] else -1
    fl = []
    supported_extensions = [x.data().decode() for x in QImageReader.supportedImageFormats()]
    fre = re.compile("^.*\\.(" + "|".join(supported_extensions) + ")$", re.IGNORECASE)
    for f in os.listdir(filelist[0][:slash+1] + "."):
        if fre.match(f):
            fl.append(filelist[0][:slash+1] + f)
    fl.sort(key = smartSort)
    if filelist[0] in fl:
        i = fl.index(filelist[0])
        filelist = fl if i == 0 else fl[i:] + fl[:i]
aa = MyDialog(None, filelist)
aa.exec_()
aa = None

Last edited by xerxes_ (2020-03-24 12:38:38)

Offline

Board footer

Powered by FluxBB