summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Doc/whatsnew/3.13.rst6
-rw-r--r--Lib/test/test_tkinter/test_images.py106
-rw-r--r--Lib/tkinter/__init__.py122
-rw-r--r--Misc/NEWS.d/next/Library/2024-04-25-11-49-11.gh-issue-118271.5N2Xcy.rst4
4 files changed, 222 insertions, 16 deletions
diff --git a/Doc/whatsnew/3.13.rst b/Doc/whatsnew/3.13.rst
index 67e7fbe..d573d81 100644
--- a/Doc/whatsnew/3.13.rst
+++ b/Doc/whatsnew/3.13.rst
@@ -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
---------
diff --git a/Lib/test/test_tkinter/test_images.py b/Lib/test/test_tkinter/test_images.py
index 2a59d01..b8e549e 100644
--- a/Lib/test/test_tkinter/test_images.py
+++ b/Lib/test/test_tkinter/test_images.py
@@ -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)
diff --git a/Lib/tkinter/__init__.py b/Lib/tkinter/__init__.py
index 35da831..5031085 100644
--- a/Lib/tkinter/__init__.py
+++ b/Lib/tkinter/__init__.py
@@ -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
index 0000000..7f11602
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2024-04-25-11-49-11.gh-issue-118271.5N2Xcy.rst
@@ -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`.