diff --git a/PIL/Image.py b/PIL/Image.py index 967a36fdb..a6b08d196 100644 --- a/PIL/Image.py +++ b/PIL/Image.py @@ -1936,6 +1936,20 @@ class Image(object): im = self.im.effect_spread(distance) return self._new(im) + def toqimage(self): + """Returns a QImage copy of this image""" + from PIL import ImageQt + if not ImageQt.qt_is_installed: + raise ImportError("Qt bindings are not installed") + return ImageQt.toqimage(self) + + def toqpixmap(self): + """Returns a QPixmap copy of this image""" + from PIL import ImageQt + if not ImageQt.qt_is_installed: + raise ImportError("Qt bindings are not installed") + return ImageQt.toqpixmap(self) + # -------------------------------------------------------------------- # Lazy operations @@ -2185,6 +2199,22 @@ def fromarray(obj, mode=None): return frombuffer(mode, size, obj, "raw", rawmode, 0, 1) + +def fromqimage(im): + """Creates an image instance from a QImage image""" + from PIL import ImageQt + if not ImageQt.qt_is_installed: + raise ImportError("Qt bindings are not installed") + return ImageQt.fromqimage(im) + + +def fromqpixmap(im): + """Creates an image instance from a QPixmap image""" + from PIL import ImageQt + if not ImageQt.qt_is_installed: + raise ImportError("Qt bindings are not installed") + return ImageQt.fromqpixmap(im) + _fromarray_typemap = { # (shape, typestr) => mode, rawmode # first two members of shape are set to one diff --git a/PIL/ImageQt.py b/PIL/ImageQt.py index 6b7d4d66e..0e8cee009 100644 --- a/PIL/ImageQt.py +++ b/PIL/ImageQt.py @@ -16,87 +16,149 @@ # See the README file for information on usage and redistribution. # -from PIL import Image +import PIL from PIL._util import isPath -import sys +from io import BytesIO -if 'PyQt4.QtGui' not in sys.modules: +qt_is_installed = True +qt_version = None +try: + from PyQt5.QtGui import QImage, qRgba, QPixmap + from PyQt5.QtCore import QBuffer, QIODevice + qt_version = '5' +except ImportError: try: - from PyQt5.QtGui import QImage, qRgba - except: + from PyQt4.QtGui import QImage, qRgba, QPixmap + from PyQt4.QtCore import QBuffer, QIODevice + qt_version = '4' + except ImportError: try: - from PyQt4.QtGui import QImage, qRgba - except: - from PySide.QtGui import QImage, qRgba - -else: #PyQt4 is used - from PyQt4.QtGui import QImage, qRgba - -## -# (Internal) Turns an RGB color into a Qt compatible color integer. + from PySide.QtGui import QImage, qRgba, QPixmap + from PySide.QtCore import QBuffer, QIODevice + qt_version = 'side' + except ImportError: + qt_is_installed = False def rgb(r, g, b, a=255): + """(Internal) Turns an RGB color into a Qt compatible color integer.""" # use qRgb to pack the colors, and then turn the resulting long # into a negative integer with the same bitpattern. return (qRgba(r, g, b, a) & 0xffffffff) +# :param im A PIL Image object, or a file name +# (given either as Python string or a PyQt string object) + +def fromqimage(im): + buffer = QBuffer() + buffer.open(QIODevice.ReadWrite) + im.save(buffer, 'ppm') + + b = BytesIO() + try: + b.write(buffer.data()) + except TypeError: + # workaround for Python 2 + b.write(str(buffer.data())) + buffer.close() + b.seek(0) + + return PIL.Image.open(b) + + +def fromqpixmap(im): + return fromqimage(im) + # buffer = QBuffer() + # buffer.open(QIODevice.ReadWrite) + # # im.save(buffer) + # # What if png doesn't support some image features like animation? + # im.save(buffer, 'ppm') + # bytes_io = BytesIO() + # bytes_io.write(buffer.data()) + # buffer.close() + # bytes_io.seek(0) + # return PIL.Image.open(bytes_io) + + +def _toqclass_helper(im): + data = None + colortable = None + + # handle filename, if given instead of image name + if hasattr(im, "toUtf8"): + # FIXME - is this really the best way to do this? + if str is bytes: + im = unicode(im.toUtf8(), "utf-8") + else: + im = str(im.toUtf8(), "utf-8") + if isPath(im): + im = PIL.Image.open(im) + + if im.mode == "1": + format = QImage.Format_Mono + elif im.mode == "L": + format = QImage.Format_Indexed8 + colortable = [] + for i in range(256): + colortable.append(rgb(i, i, i)) + elif im.mode == "P": + format = QImage.Format_Indexed8 + colortable = [] + palette = im.getpalette() + for i in range(0, len(palette), 3): + colortable.append(rgb(*palette[i:i+3])) + elif im.mode == "RGB": + data = im.tobytes("raw", "BGRX") + format = QImage.Format_RGB32 + elif im.mode == "RGBA": + try: + data = im.tobytes("raw", "BGRA") + except SystemError: + # workaround for earlier versions + r, g, b, a = im.split() + im = PIL.Image.merge("RGBA", (b, g, r, a)) + format = QImage.Format_ARGB32 + else: + raise ValueError("unsupported image mode %r" % im.mode) + + # must keep a reference, or Qt will crash! + __data = data or im.tobytes() + return { + 'data': __data, 'im': im, 'format': format, 'colortable': colortable + } + ## -# An PIL image wrapper for Qt. This is a subclass of PyQt4's QImage +# An PIL image wrapper for Qt. This is a subclass of PyQt's QImage # class. # # @param im A PIL Image object, or a file name (given either as Python # string or a PyQt string object). -class ImageQt(QImage): +if qt_is_installed: + class ImageQt(QImage): - def __init__(self, im): + def __init__(self, im): + im_data = _toqclass_helper(im) + QImage.__init__(self, + im_data['data'], im_data['im'].size[0], + im_data['im'].size[1], im_data['format']) + if im_data['colortable']: + self.setColorTable(im_data['colortable']) - data = None - colortable = None - # handle filename, if given instead of image name - if hasattr(im, "toUtf8"): - # FIXME - is this really the best way to do this? - if str is bytes: - im = unicode(im.toUtf8(), "utf-8") - else: - im = str(im.toUtf8(), "utf-8") - if isPath(im): - im = Image.open(im) +def toqimage(im): + return ImageQt(im) - if im.mode == "1": - format = QImage.Format_Mono - elif im.mode == "L": - format = QImage.Format_Indexed8 - colortable = [] - for i in range(256): - colortable.append(rgb(i, i, i)) - elif im.mode == "P": - format = QImage.Format_Indexed8 - colortable = [] - palette = im.getpalette() - for i in range(0, len(palette), 3): - colortable.append(rgb(*palette[i:i+3])) - elif im.mode == "RGB": - data = im.tobytes("raw", "BGRX") - format = QImage.Format_RGB32 - elif im.mode == "RGBA": - try: - data = im.tobytes("raw", "BGRA") - except SystemError: - # workaround for earlier versions - r, g, b, a = im.split() - im = Image.merge("RGBA", (b, g, r, a)) - format = QImage.Format_ARGB32 - else: - raise ValueError("unsupported image mode %r" % im.mode) - # must keep a reference, or Qt will crash! - self.__data = data or im.tobytes() +def toqpixmap(im): + # # This doesn't work. For now using a dumb approach. + # im_data = _toqclass_helper(im) + # result = QPixmap(im_data['im'].size[0], im_data['im'].size[1]) + # result.loadFromData(im_data['data']) + # Fix some strange bug that causes + if im.mode == 'RGB': + im = im.convert('RGBA') - QImage.__init__(self, self.__data, im.size[0], im.size[1], format) - - if colortable: - self.setColorTable(colortable) + qimage = toqimage(im) + return QPixmap.fromImage(qimage) diff --git a/Tests/test_image_fromqimage.py b/Tests/test_image_fromqimage.py new file mode 100644 index 000000000..957ec9cc1 --- /dev/null +++ b/Tests/test_image_fromqimage.py @@ -0,0 +1,33 @@ +from helper import unittest, PillowTestCase, hopper +from test_imageqt import PillowQtTestCase + +from PIL import ImageQt + + +class TestFromQImage(PillowQtTestCase, PillowTestCase): + + def roundtrip(self, expected): + result = ImageQt.fromqimage(expected.toqimage()) + # Qt saves all images as rgb + self.assert_image_equal(result, expected.convert('RGB')) + + def test_sanity_1(self): + self.roundtrip(hopper('1')) + + def test_sanity_rgb(self): + self.roundtrip(hopper('RGB')) + + def test_sanity_rgba(self): + self.roundtrip(hopper('RGBA')) + + def test_sanity_l(self): + self.roundtrip(hopper('L')) + + def test_sanity_p(self): + self.roundtrip(hopper('P')) + + +if __name__ == '__main__': + unittest.main() + +# End of file diff --git a/Tests/test_image_fromqpixmap.py b/Tests/test_image_fromqpixmap.py new file mode 100644 index 000000000..78e4d6770 --- /dev/null +++ b/Tests/test_image_fromqpixmap.py @@ -0,0 +1,34 @@ +from helper import unittest, PillowTestCase, hopper +from test_imageqt import PillowQtTestCase, PillowQPixmapTestCase + +from PIL import ImageQt + + +class TestFromQPixmap(PillowQPixmapTestCase, PillowTestCase): + + def roundtrip(self, expected): + PillowQtTestCase.setUp(self) + result = ImageQt.fromqpixmap(ImageQt.toqpixmap(expected)) + # Qt saves all pixmaps as rgb + self.assert_image_equal(result, expected.convert('RGB')) + + def test_sanity_1(self): + self.roundtrip(hopper('1')) + + def test_sanity_rgb(self): + self.roundtrip(hopper('RGB')) + + def test_sanity_rgba(self): + self.roundtrip(hopper('RGBA')) + + def test_sanity_l(self): + self.roundtrip(hopper('L')) + + def test_sanity_p(self): + self.roundtrip(hopper('P')) + + +if __name__ == '__main__': + unittest.main() + +# End of file diff --git a/Tests/test_image_toqimage.py b/Tests/test_image_toqimage.py new file mode 100644 index 000000000..38b83c023 --- /dev/null +++ b/Tests/test_image_toqimage.py @@ -0,0 +1,29 @@ +from helper import unittest, PillowTestCase, hopper +from test_imageqt import PillowQtTestCase + +from PIL import ImageQt + + +if ImageQt.qt_is_installed: + from PIL.ImageQt import QImage + + +class TestToQImage(PillowQtTestCase, PillowTestCase): + + def test_sanity(self): + PillowQtTestCase.setUp(self) + for mode in ('1', 'RGB', 'RGBA', 'L', 'P'): + data = ImageQt.toqimage(hopper(mode)) + + self.assertTrue(isinstance(data, QImage)) + self.assertFalse(data.isNull()) + + # Test saving the file + tempfile = self.tempfile('temp_{}.png'.format(mode)) + data.save(tempfile) + + +if __name__ == '__main__': + unittest.main() + +# End of file diff --git a/Tests/test_image_toqpixmap.py b/Tests/test_image_toqpixmap.py new file mode 100644 index 000000000..95db2e8f7 --- /dev/null +++ b/Tests/test_image_toqpixmap.py @@ -0,0 +1,29 @@ +from helper import unittest, PillowTestCase, hopper +from test_imageqt import PillowQtTestCase, PillowQPixmapTestCase + +from PIL import ImageQt + +if ImageQt.qt_is_installed: + from PIL.ImageQt import QPixmap + + +class TestToQPixmap(PillowQPixmapTestCase, PillowTestCase): + + def test_sanity(self): + PillowQtTestCase.setUp(self) + + for mode in ('1', 'RGB', 'RGBA', 'L', 'P'): + data = ImageQt.toqpixmap(hopper(mode)) + + self.assertTrue(isinstance(data, QPixmap)) + self.assertFalse(data.isNull()) + + # Test saving the file + tempfile = self.tempfile('temp_{}.png'.format(mode)) + data.save(tempfile) + + +if __name__ == '__main__': + unittest.main() + +# End of file diff --git a/Tests/test_imageqt.py b/Tests/test_imageqt.py index ecab9a956..b0fad6a45 100644 --- a/Tests/test_imageqt.py +++ b/Tests/test_imageqt.py @@ -1,38 +1,61 @@ from helper import unittest, PillowTestCase, hopper -try: - from PIL import ImageQt - from PyQt5.QtGui import QImage, qRgb, qRgba -except: - try: - from PyQt4.QtGui import QImage, qRgb, qRgba - except: - try: - from PySide.QtGui import QImage, qRgb, qRgba - except: - # Will be skipped in setUp - pass +from PIL import ImageQt -class TestImageQt(PillowTestCase): +if ImageQt.qt_is_installed: + from PIL.ImageQt import qRgba + + def skip_if_qt_is_not_installed(_): + pass +else: + def skip_if_qt_is_not_installed(test_case): + test_case.skipTest('Qt bindings are not installed') + + +class PillowQtTestCase(object): def setUp(self): + skip_if_qt_is_not_installed(self) + + def tearDown(self): + pass + + +class PillowQPixmapTestCase(PillowQtTestCase): + + def setUp(self): + PillowQtTestCase.setUp(self) try: - from PyQt5.QtGui import QImage, qRgb, qRgba + if ImageQt.qt_version == '5': + from PyQt5.QtGui import QGuiApplication + elif ImageQt.qt_version == '4': + from PyQt4.QtGui import QGuiApplication + elif ImageQt.qt_version == 'side': + from PySide.QtGui import QGuiApplication except ImportError: - try: - from PyQt4.QtGui import QImage, qRgb, qRgba - except ImportError: - try: - from PySide.QtGui import QImage, qRgb, qRgba - except ImportError: - self.skipTest('PyQt4 or 5 or PySide not installed') + self.skipTest('QGuiApplication not installed') + + self.app = QGuiApplication([]) + + def tearDown(self): + PillowQtTestCase.tearDown(self) + self.app.quit() + + +class TestImageQt(PillowQtTestCase, PillowTestCase): def test_rgb(self): - # from https://qt-project.org/doc/qt-4.8/qcolor.html + # from https://doc.qt.io/qt-4.8/qcolor.html # typedef QRgb # An ARGB quadruplet on the format #AARRGGBB, # equivalent to an unsigned int. + if ImageQt.qt_version == '5': + from PyQt5.QtGui import qRgb + elif ImageQt.qt_version == '4': + from PyQt4.QtGui import qRgb + elif ImageQt.qt_version == 'side': + from PySide.QtGui import qRgb self.assertEqual(qRgb(0, 0, 0), qRgba(0, 0, 0, 255))