]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-118271: Support more options for reading/writing images in Tkinter (GH-118273)
authorSerhiy Storchaka <storchaka@gmail.com>
Mon, 6 May 2024 15:06:06 +0000 (18:06 +0300)
committerGitHub <noreply@github.com>
Mon, 6 May 2024 15:06:06 +0000 (15:06 +0000)
* Add PhotoImage.read() to read an image from a file.
* Add PhotoImage.data() to get the image data.
* Add background and grayscale parameters to PhotoImage.write().

Doc/whatsnew/3.13.rst
Lib/test/test_tkinter/test_images.py
Lib/tkinter/__init__.py
Misc/NEWS.d/next/Library/2024-04-25-11-49-11.gh-issue-118271.5N2Xcy.rst [new file with mode: 0644]

index 67e7fbebc92ef46851381f2e8654673f3ec0693b..d573d8109edef861fb8b7e8dcee9b4f3ebe947bb 100644 (file)
@@ -891,6 +891,12 @@ tkinter
   :meth:`!copy()`.
   (Contributed by Serhiy Storchaka in :gh:`118225`.)
 
+* Add the :class:`!PhotoImage` methods :meth:`!read` to read
+  an image from a file and :meth:`!data` to get the image data.
+  Add *background* and *grayscale* parameters to :class:`!PhotoImage` method
+  :meth:`!write`.
+  (Contributed by Serhiy Storchaka in :gh:`118271`.)
+
 traceback
 ---------
 
index 2a59d014f4aa2a0ca1eb5e2d37304ba21c345ce8..b8e549e314d27d9b28ed34420c70839f1a2169be 100644 (file)
@@ -505,6 +505,50 @@ class PhotoImageTest(AbstractTkTest, unittest.TestCase):
         self.assertRaises(tkinter.TclError, image.get, 16, 15)
         self.assertRaises(tkinter.TclError, image.get, 15, 16)
 
+    def test_read(self):
+        # Due to the Tk bug https://core.tcl-lang.org/tk/tktview/1576528
+        # the -from option does not work correctly for GIF and PNG files.
+        # Use the PPM file for this test.
+        testfile = support.findfile('python.ppm', subdir='tkinterdata')
+        image = tkinter.PhotoImage(master=self.root, file=testfile)
+
+        image2 = tkinter.PhotoImage(master=self.root)
+        image2.read(testfile)
+        self.assertEqual(image2.type(), 'photo')
+        self.assertEqual(image2.width(), 16)
+        self.assertEqual(image2.height(), 16)
+        self.assertEqual(image2.get(0, 0), image.get(0, 0))
+        self.assertEqual(image2.get(4, 6), image.get(4, 6))
+
+        self.assertRaises(tkinter.TclError, image2.read, self.testfile, 'ppm')
+
+        image2 = tkinter.PhotoImage(master=self.root)
+        image2.read(testfile, from_coords=(2, 3, 14, 11))
+        self.assertEqual(image2.width(), 12)
+        self.assertEqual(image2.height(), 8)
+        self.assertEqual(image2.get(0, 0), image.get(2, 3))
+        self.assertEqual(image2.get(11, 7), image.get(13, 10))
+        self.assertEqual(image2.get(2, 4), image.get(2+2, 4+3))
+
+        image2 = tkinter.PhotoImage(master=self.root, file=testfile)
+        self.assertEqual(image2.width(), 16)
+        self.assertEqual(image2.height(), 16)
+        image2.read(testfile, from_coords=(2, 3, 14, 11), shrink=True)
+        self.assertEqual(image2.width(), 12)
+        self.assertEqual(image2.height(), 8)
+        self.assertEqual(image2.get(0, 0), image.get(2, 3))
+        self.assertEqual(image2.get(11, 7), image.get(13, 10))
+        self.assertEqual(image2.get(2, 4), image.get(2+2, 4+3))
+
+        image2 = tkinter.PhotoImage(master=self.root)
+        image2.read(testfile, from_coords=(2, 3, 14, 11), to=(3, 6))
+        self.assertEqual(image2.type(), 'photo')
+        self.assertEqual(image2.width(), 15)
+        self.assertEqual(image2.height(), 14)
+        self.assertEqual(image2.get(0+3, 0+6), image.get(2, 3))
+        self.assertEqual(image2.get(11+3, 7+6), image.get(13, 10))
+        self.assertEqual(image2.get(2+3, 4+6), image.get(2+2, 4+3))
+
     def test_write(self):
         filename = os_helper.TESTFN
         import locale
@@ -516,19 +560,58 @@ class PhotoImageTest(AbstractTkTest, unittest.TestCase):
 
         image.write(filename)
         image2 = tkinter.PhotoImage('::img::test2', master=self.root,
-                                    format='ppm',
-                                    file=filename)
+                                    format='ppm', file=filename)
         self.assertEqual(str(image2), '::img::test2')
         self.assertEqual(image2.type(), 'photo')
         self.assertEqual(image2.width(), 16)
         self.assertEqual(image2.height(), 16)
         self.assertEqual(image2.get(0, 0), image.get(0, 0))
-        self.assertEqual(image2.get(15, 8), image.get(15, 8))
+        self.assertEqual(image2.get(4, 6), image.get(4, 6))
 
         image.write(filename, format='gif', from_coords=(4, 6, 6, 9))
         image3 = tkinter.PhotoImage('::img::test3', master=self.root,
-                                    format='gif',
-                                    file=filename)
+                                    format='gif', file=filename)
+        self.assertEqual(str(image3), '::img::test3')
+        self.assertEqual(image3.type(), 'photo')
+        self.assertEqual(image3.width(), 2)
+        self.assertEqual(image3.height(), 3)
+        self.assertEqual(image3.get(0, 0), image.get(4, 6))
+        self.assertEqual(image3.get(1, 2), image.get(5, 8))
+
+        image.write(filename, background='#ff0000')
+        image4 = tkinter.PhotoImage('::img::test4', master=self.root,
+                                    format='ppm', file=filename)
+        self.assertEqual(image4.get(0, 0), (255, 0, 0))
+        self.assertEqual(image4.get(4, 6), image.get(4, 6))
+
+        image.write(filename, grayscale=True)
+        image5 = tkinter.PhotoImage('::img::test5', master=self.root,
+                                    format='ppm', file=filename)
+        c = image5.get(4, 6)
+        self.assertTrue(c[0] == c[1] == c[2], c)
+
+    def test_data(self):
+        image = self.create()
+
+        data = image.data()
+        self.assertIsInstance(data, tuple)
+        for row in data:
+            self.assertIsInstance(row, str)
+        self.assertEqual(data[6].split()[4], '#%02x%02x%02x' % image.get(4, 6))
+
+        data = image.data('ppm')
+        image2 = tkinter.PhotoImage('::img::test2', master=self.root,
+                                    format='ppm', data=data)
+        self.assertEqual(str(image2), '::img::test2')
+        self.assertEqual(image2.type(), 'photo')
+        self.assertEqual(image2.width(), 16)
+        self.assertEqual(image2.height(), 16)
+        self.assertEqual(image2.get(0, 0), image.get(0, 0))
+        self.assertEqual(image2.get(4, 6), image.get(4, 6))
+
+        data = image.data(format='gif', from_coords=(4, 6, 6, 9))
+        image3 = tkinter.PhotoImage('::img::test3', master=self.root,
+                                    format='gif', data=data)
         self.assertEqual(str(image3), '::img::test3')
         self.assertEqual(image3.type(), 'photo')
         self.assertEqual(image3.width(), 2)
@@ -536,6 +619,19 @@ class PhotoImageTest(AbstractTkTest, unittest.TestCase):
         self.assertEqual(image3.get(0, 0), image.get(4, 6))
         self.assertEqual(image3.get(1, 2), image.get(5, 8))
 
+        data = image.data('ppm', background='#ff0000')
+        image4 = tkinter.PhotoImage('::img::test4', master=self.root,
+                                    format='ppm', data=data)
+        self.assertEqual(image4.get(0, 0), (255, 0, 0))
+        self.assertEqual(image4.get(4, 6), image.get(4, 6))
+
+        data = image.data('ppm', grayscale=True)
+        image5 = tkinter.PhotoImage('::img::test5', master=self.root,
+                                    format='ppm', data=data)
+        c = image5.get(4, 6)
+        self.assertTrue(c[0] == c[1] == c[2], c)
+
+
     def test_transparency(self):
         image = self.create()
         self.assertEqual(image.transparency_get(0, 0), True)
index 35da83107bd99ef29099b12e765947e9503d82e5..5031085ac3e9279b8f9e97c5c712e60ee9cdeff1 100644 (file)
@@ -4398,17 +4398,117 @@ class PhotoImage(Image):
                 to = to[1:]
             args = args + ('-to',) + tuple(to)
         self.tk.call(args)
-    # XXX read
-
-    def write(self, filename, format=None, from_coords=None):
-        """Write image to file FILENAME in FORMAT starting from
-        position FROM_COORDS."""
-        args = (self.name, 'write', filename)
-        if format:
-            args = args + ('-format', format)
-        if from_coords:
-            args = args + ('-from',) + tuple(from_coords)
-        self.tk.call(args)
+
+    def read(self, filename, format=None, *, from_coords=None, to=None, shrink=False):
+        """Reads image data from the file named FILENAME into the image.
+
+        The FORMAT option specifies the format of the image data in the
+        file.
+
+        The FROM_COORDS option specifies a rectangular sub-region of the image
+        file data to be copied to the destination image.  It must be a tuple
+        or a list of 1 to 4 integers (x1, y1, x2, y2).  (x1, y1) and
+        (x2, y2) specify diagonally opposite corners of the rectangle.  If
+        x2 and y2 are not specified, the default value is the bottom-right
+        corner of the source image.  The default, if this option is not
+        specified, is the whole of the image in the image file.
+
+        The TO option specifies the coordinates of the top-left corner of
+        the region of the image into which data from filename are to be
+        read.  The default is (0, 0).
+
+        If SHRINK is true, the size of the destination image will be
+        reduced, if necessary, so that the region into which the image file
+        data are read is at the bottom-right corner of the image.
+        """
+        options = ()
+        if format is not None:
+            options += ('-format', format)
+        if from_coords is not None:
+            options += ('-from', *from_coords)
+        if shrink:
+            options += ('-shrink',)
+        if to is not None:
+            options += ('-to', *to)
+        self.tk.call(self.name, 'read', filename, *options)
+
+    def write(self, filename, format=None, from_coords=None, *,
+              background=None, grayscale=False):
+        """Writes image data from the image to a file named FILENAME.
+
+        The FORMAT option specifies the name of the image file format
+        handler to be used to write the data to the file.  If this option
+        is not given, the format is guessed from the file extension.
+
+        The FROM_COORDS option specifies a rectangular region of the image
+        to be written to the image file.  It must be a tuple or a list of 1
+        to 4 integers (x1, y1, x2, y2).  If only x1 and y1 are specified,
+        the region extends from (x1,y1) to the bottom-right corner of the
+        image.  If all four coordinates are given, they specify diagonally
+        opposite corners of the rectangular region.  The default, if this
+        option is not given, is the whole image.
+
+        If BACKGROUND is specified, the data will not contain any
+        transparency information.  In all transparent pixels the color will
+        be replaced by the specified color.
+
+        If GRAYSCALE is true, the data will not contain color information.
+        All pixel data will be transformed into grayscale.
+        """
+        options = ()
+        if format is not None:
+            options += ('-format', format)
+        if from_coords is not None:
+            options += ('-from', *from_coords)
+        if grayscale:
+            options += ('-grayscale',)
+        if background is not None:
+            options += ('-background', background)
+        self.tk.call(self.name, 'write', filename, *options)
+
+    def data(self, format=None, *, from_coords=None,
+             background=None, grayscale=False):
+        """Returns image data.
+
+        The FORMAT option specifies the name of the image file format
+        handler to be used.  If this option is not given, this method uses
+        a format that consists of a tuple (one element per row) of strings
+        containings space separated (one element per pixel/column) colors
+        in “#RRGGBB” format (where RR is a pair of hexadecimal digits for
+        the red channel, GG for green, and BB for blue).
+
+        The FROM_COORDS option specifies a rectangular region of the image
+        to be returned.  It must be a tuple or a list of 1 to 4 integers
+        (x1, y1, x2, y2).  If only x1 and y1 are specified, the region
+        extends from (x1,y1) to the bottom-right corner of the image.  If
+        all four coordinates are given, they specify diagonally opposite
+        corners of the rectangular region, including (x1, y1) and excluding
+        (x2, y2).  The default, if this option is not given, is the whole
+        image.
+
+        If BACKGROUND is specified, the data will not contain any
+        transparency information.  In all transparent pixels the color will
+        be replaced by the specified color.
+
+        If GRAYSCALE is true, the data will not contain color information.
+        All pixel data will be transformed into grayscale.
+        """
+        options = ()
+        if format is not None:
+            options += ('-format', format)
+        if from_coords is not None:
+            options += ('-from', *from_coords)
+        if grayscale:
+            options += ('-grayscale',)
+        if background is not None:
+            options += ('-background', background)
+        data = self.tk.call(self.name, 'data', *options)
+        if isinstance(data, str):  # For wantobjects = 0.
+            if format is None:
+                data = self.tk.splitlist(data)
+            else:
+                data = bytes(data, 'latin1')
+        return data
 
     def transparency_get(self, x, y):
         """Return True if the pixel at x,y is transparent."""
diff --git a/Misc/NEWS.d/next/Library/2024-04-25-11-49-11.gh-issue-118271.5N2Xcy.rst b/Misc/NEWS.d/next/Library/2024-04-25-11-49-11.gh-issue-118271.5N2Xcy.rst
new file mode 100644 (file)
index 0000000..7f11602
--- /dev/null
@@ -0,0 +1,4 @@
+Add the :class:`!PhotoImage` methods :meth:`~tkinter.PhotoImage.read` to
+read an image from a file and :meth:`~tkinter.PhotoImage.data` to get the
+image data. Add *background* and *grayscale* parameters to
+:class:`!PhotoImage` method :meth:`~tkinter.PhotoImage.write`.