1#!/usr/bin/env python
2
3
4#############################################################################
5##
6## Copyright (C) 2017 Riverbank Computing Limited.
7## Copyright (C) 2010 Nokia Corporation and/or its subsidiary(-ies).
8## All rights reserved.
9##
10## This file is part of the examples of PyQt.
11##
12## $QT_BEGIN_LICENSE:BSD$
13## You may use this file under the terms of the BSD license as follows:
14##
15## "Redistribution and use in source and binary forms, with or without
16## modification, are permitted provided that the following conditions are
17## met:
18##   * Redistributions of source code must retain the above copyright
19##     notice, this list of conditions and the following disclaimer.
20##   * Redistributions in binary form must reproduce the above copyright
21##     notice, this list of conditions and the following disclaimer in
22##     the documentation and/or other materials provided with the
23##     distribution.
24##   * Neither the name of Nokia Corporation and its Subsidiary(-ies) nor
25##     the names of its contributors may be used to endorse or promote
26##     products derived from this software without specific prior written
27##     permission.
28##
29## THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
30## "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
31## LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
32## A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
33## OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
34## SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
35## LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
36## DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
37## THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
38## (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
39## OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
40## $QT_END_LICENSE$
41##
42#############################################################################
43
44
45from PyQt5.QtCore import (QAbstractTableModel, QDir, QModelIndex, QRect,
46        QRectF, QSize, Qt)
47from PyQt5.QtGui import QBrush, qGray, QImage, QPainter
48from PyQt5.QtPrintSupport import QPrintDialog, QPrinter
49from PyQt5.QtWidgets import (QAbstractItemDelegate, QApplication, QDialog,
50        QFileDialog, QHBoxLayout, QLabel, QMainWindow, QMessageBox, QMenu,
51        QProgressDialog, QSpinBox, QStyle, QStyleOptionViewItem, QTableView,
52        QVBoxLayout, QWidget)
53
54import pixelator_rc
55
56
57ItemSize = 256
58
59
60class PixelDelegate(QAbstractItemDelegate):
61    def __init__(self, parent=None):
62        super(PixelDelegate, self).__init__(parent)
63
64        self.pixelSize = 12
65
66    def paint(self, painter, option, index):
67        if option.state & QStyle.State_Selected:
68            painter.fillRect(option.rect, option.palette.highlight())
69
70        size = min(option.rect.width(), option.rect.height())
71        brightness = index.model().data(index, Qt.DisplayRole)
72        radius = (size/2.0) - (brightness/255.0 * size/2.0)
73        if radius == 0.0:
74            return
75
76        painter.save()
77        painter.setRenderHint(QPainter.Antialiasing)
78        painter.setPen(Qt.NoPen)
79
80        if option.state & QStyle.State_Selected:
81            painter.setBrush(option.palette.highlightedText())
82        else:
83            painter.setBrush(QBrush(Qt.black))
84
85        painter.drawEllipse(QRectF(
86                            option.rect.x() + option.rect.width()/2 - radius,
87                            option.rect.y() + option.rect.height()/2 - radius,
88                            2*radius, 2*radius))
89
90        painter.restore()
91
92    def sizeHint(self, option, index):
93        return QSize(self.pixelSize, self.pixelSize)
94
95    def setPixelSize(self, size):
96        self.pixelSize = size
97
98
99class ImageModel(QAbstractTableModel):
100    def __init__(self, parent=None):
101        super(ImageModel, self).__init__(parent)
102
103        self.modelImage = QImage()
104
105    def setImage(self, image):
106        self.beginResetModel()
107        self.modelImage = QImage(image)
108        self.endResetModel()
109
110    def rowCount(self, parent):
111        return self.modelImage.height()
112
113    def columnCount(self, parent):
114        return self.modelImage.width()
115
116    def data(self, index, role):
117        if not index.isValid() or role != Qt.DisplayRole:
118            return None
119
120        return qGray(self.modelImage.pixel(index.column(), index.row()))
121
122    def headerData(self, section, orientation, role):
123        if role == Qt.SizeHintRole:
124            return QSize(1, 1)
125
126        return None
127
128
129class MainWindow(QMainWindow):
130    def __init__(self):
131        super(MainWindow, self).__init__()
132
133        self.currentPath = QDir.homePath()
134        self.model = ImageModel(self)
135
136        centralWidget = QWidget()
137
138        self.view = QTableView()
139        self.view.setShowGrid(False)
140        self.view.horizontalHeader().hide()
141        self.view.verticalHeader().hide()
142        self.view.horizontalHeader().setMinimumSectionSize(1)
143        self.view.verticalHeader().setMinimumSectionSize(1)
144        self.view.setModel(self.model)
145
146        delegate = PixelDelegate(self)
147        self.view.setItemDelegate(delegate)
148
149        pixelSizeLabel = QLabel("Pixel size:")
150        pixelSizeSpinBox = QSpinBox()
151        pixelSizeSpinBox.setMinimum(4)
152        pixelSizeSpinBox.setMaximum(32)
153        pixelSizeSpinBox.setValue(12)
154
155        fileMenu = QMenu("&File", self)
156        openAction = fileMenu.addAction("&Open...")
157        openAction.setShortcut("Ctrl+O")
158
159        self.printAction = fileMenu.addAction("&Print...")
160        self.printAction.setEnabled(False)
161        self.printAction.setShortcut("Ctrl+P")
162
163        quitAction = fileMenu.addAction("E&xit")
164        quitAction.setShortcut("Ctrl+Q")
165
166        helpMenu = QMenu("&Help", self)
167        aboutAction = helpMenu.addAction("&About")
168
169        self.menuBar().addMenu(fileMenu)
170        self.menuBar().addSeparator()
171        self.menuBar().addMenu(helpMenu)
172
173        openAction.triggered.connect(self.chooseImage)
174        self.printAction.triggered.connect(self.printImage)
175        quitAction.triggered.connect(QApplication.instance().quit)
176        aboutAction.triggered.connect(self.showAboutBox)
177        pixelSizeSpinBox.valueChanged.connect(delegate.setPixelSize)
178        pixelSizeSpinBox.valueChanged.connect(self.updateView)
179
180        controlsLayout = QHBoxLayout()
181        controlsLayout.addWidget(pixelSizeLabel)
182        controlsLayout.addWidget(pixelSizeSpinBox)
183        controlsLayout.addStretch(1)
184
185        mainLayout = QVBoxLayout()
186        mainLayout.addWidget(self.view)
187        mainLayout.addLayout(controlsLayout)
188        centralWidget.setLayout(mainLayout)
189
190        self.setCentralWidget(centralWidget)
191
192        self.setWindowTitle("Pixelator")
193        self.resize(640, 480)
194
195    def chooseImage(self):
196        fileName, _ = QFileDialog.getOpenFileName(self, "Choose an Image",
197                self.currentPath, '*')
198
199        if fileName:
200            self.openImage(fileName)
201
202    def openImage(self, fileName):
203        image = QImage()
204
205        if image.load(fileName):
206            self.model.setImage(image)
207
208            if not fileName.startswith(':/'):
209                self.currentPath = fileName
210                self.setWindowTitle("%s - Pixelator" % self.currentPath)
211
212            self.printAction.setEnabled(True)
213            self.updateView()
214
215    def printImage(self):
216        if self.model.rowCount(QModelIndex()) * self.model.columnCount(QModelIndex()) > 90000:
217            answer = QMessageBox.question(self, "Large Image Size",
218                    "The printed image may be very large. Are you sure that "
219                    "you want to print it?",
220                    QMessageBox.Yes | QMessageBox.No)
221            if answer == QMessageBox.No:
222                return
223
224        printer = QPrinter(QPrinter.HighResolution)
225
226        dlg = QPrintDialog(printer, self)
227        dlg.setWindowTitle("Print Image")
228
229        if dlg.exec_() != QDialog.Accepted:
230            return
231
232        painter = QPainter()
233        painter.begin(printer)
234
235        rows = self.model.rowCount(QModelIndex())
236        columns = self.model.columnCount(QModelIndex())
237        sourceWidth = (columns+1) * ItemSize
238        sourceHeight = (rows+1) * ItemSize
239
240        painter.save()
241
242        xscale = printer.pageRect().width() / float(sourceWidth)
243        yscale = printer.pageRect().height() / float(sourceHeight)
244        scale = min(xscale, yscale)
245
246        painter.translate(printer.paperRect().x()+printer.pageRect().width()/2,
247                          printer.paperRect().y()+printer.pageRect().height()/2)
248        painter.scale(scale, scale)
249        painter.translate(-sourceWidth/2, -sourceHeight/2)
250
251        option = QStyleOptionViewItem()
252        parent = QModelIndex()
253
254        progress = QProgressDialog("Printing...", "Cancel", 0, rows, self)
255        progress.setWindowModality(Qt.ApplicationModal)
256        y = ItemSize / 2.0
257
258        for row in range(rows):
259            progress.setValue(row)
260            QApplication.processEvents()
261            if progress.wasCanceled():
262                break
263
264            x = ItemSize / 2.0
265
266            for column in range(columns):
267                option.rect = QRect(x, y, ItemSize, ItemSize)
268                self.view.itemDelegate().paint(painter, option,
269                        self.model.index(row, column, parent))
270                x += ItemSize
271
272            y += ItemSize
273
274        progress.setValue(rows)
275
276        painter.restore()
277        painter.end()
278
279        if progress.wasCanceled():
280            QMessageBox.information(self, "Printing canceled",
281                    "The printing process was canceled.", QMessageBox.Cancel)
282
283    def showAboutBox(self):
284        QMessageBox.about(self, "About the Pixelator example",
285                "This example demonstrates how a standard view and a custom\n"
286                "delegate can be used to produce a specialized "
287                "representation\nof data in a simple custom model.")
288
289    def updateView(self):
290        self.view.resizeColumnsToContents()
291        self.view.resizeRowsToContents()
292
293
294if __name__ == '__main__':
295
296    import sys
297
298    app = QApplication(sys.argv)
299    window = MainWindow()
300    window.show()
301    window.openImage(':/images/qt.png')
302    sys.exit(app.exec_())
303