Skip to content

Commit 709ca90

Browse files
pythongh-118271: Support more options for reading/writing images in Tkinter (pythonGH-118273)
* 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().
1 parent fc50f1b commit 709ca90

File tree

4 files changed

+222
-16
lines changed

4 files changed

+222
-16
lines changed

Doc/whatsnew/3.13.rst

+6
Original file line numberDiff line numberDiff line change
@@ -891,6 +891,12 @@ tkinter
891891
:meth:`!copy()`.
892892
(Contributed by Serhiy Storchaka in :gh:`118225`.)
893893

894+
* Add the :class:`!PhotoImage` methods :meth:`!read` to read
895+
an image from a file and :meth:`!data` to get the image data.
896+
Add *background* and *grayscale* parameters to :class:`!PhotoImage` method
897+
:meth:`!write`.
898+
(Contributed by Serhiy Storchaka in :gh:`118271`.)
899+
894900
traceback
895901
---------
896902

Lib/test/test_tkinter/test_images.py

+101-5
Original file line numberDiff line numberDiff line change
@@ -505,6 +505,50 @@ def test_get(self):
505505
self.assertRaises(tkinter.TclError, image.get, 16, 15)
506506
self.assertRaises(tkinter.TclError, image.get, 15, 16)
507507

508+
def test_read(self):
509+
# Due to the Tk bug https://core.tcl-lang.org/tk/tktview/1576528
510+
# the -from option does not work correctly for GIF and PNG files.
511+
# Use the PPM file for this test.
512+
testfile = support.findfile('python.ppm', subdir='tkinterdata')
513+
image = tkinter.PhotoImage(master=self.root, file=testfile)
514+
515+
image2 = tkinter.PhotoImage(master=self.root)
516+
image2.read(testfile)
517+
self.assertEqual(image2.type(), 'photo')
518+
self.assertEqual(image2.width(), 16)
519+
self.assertEqual(image2.height(), 16)
520+
self.assertEqual(image2.get(0, 0), image.get(0, 0))
521+
self.assertEqual(image2.get(4, 6), image.get(4, 6))
522+
523+
self.assertRaises(tkinter.TclError, image2.read, self.testfile, 'ppm')
524+
525+
image2 = tkinter.PhotoImage(master=self.root)
526+
image2.read(testfile, from_coords=(2, 3, 14, 11))
527+
self.assertEqual(image2.width(), 12)
528+
self.assertEqual(image2.height(), 8)
529+
self.assertEqual(image2.get(0, 0), image.get(2, 3))
530+
self.assertEqual(image2.get(11, 7), image.get(13, 10))
531+
self.assertEqual(image2.get(2, 4), image.get(2+2, 4+3))
532+
533+
image2 = tkinter.PhotoImage(master=self.root, file=testfile)
534+
self.assertEqual(image2.width(), 16)
535+
self.assertEqual(image2.height(), 16)
536+
image2.read(testfile, from_coords=(2, 3, 14, 11), shrink=True)
537+
self.assertEqual(image2.width(), 12)
538+
self.assertEqual(image2.height(), 8)
539+
self.assertEqual(image2.get(0, 0), image.get(2, 3))
540+
self.assertEqual(image2.get(11, 7), image.get(13, 10))
541+
self.assertEqual(image2.get(2, 4), image.get(2+2, 4+3))
542+
543+
image2 = tkinter.PhotoImage(master=self.root)
544+
image2.read(testfile, from_coords=(2, 3, 14, 11), to=(3, 6))
545+
self.assertEqual(image2.type(), 'photo')
546+
self.assertEqual(image2.width(), 15)
547+
self.assertEqual(image2.height(), 14)
548+
self.assertEqual(image2.get(0+3, 0+6), image.get(2, 3))
549+
self.assertEqual(image2.get(11+3, 7+6), image.get(13, 10))
550+
self.assertEqual(image2.get(2+3, 4+6), image.get(2+2, 4+3))
551+
508552
def test_write(self):
509553
filename = os_helper.TESTFN
510554
import locale
@@ -516,26 +560,78 @@ def test_write(self):
516560

517561
image.write(filename)
518562
image2 = tkinter.PhotoImage('::img::test2', master=self.root,
519-
format='ppm',
520-
file=filename)
563+
format='ppm', file=filename)
521564
self.assertEqual(str(image2), '::img::test2')
522565
self.assertEqual(image2.type(), 'photo')
523566
self.assertEqual(image2.width(), 16)
524567
self.assertEqual(image2.height(), 16)
525568
self.assertEqual(image2.get(0, 0), image.get(0, 0))
526-
self.assertEqual(image2.get(15, 8), image.get(15, 8))
569+
self.assertEqual(image2.get(4, 6), image.get(4, 6))
527570

528571
image.write(filename, format='gif', from_coords=(4, 6, 6, 9))
529572
image3 = tkinter.PhotoImage('::img::test3', master=self.root,
530-
format='gif',
531-
file=filename)
573+
format='gif', file=filename)
574+
self.assertEqual(str(image3), '::img::test3')
575+
self.assertEqual(image3.type(), 'photo')
576+
self.assertEqual(image3.width(), 2)
577+
self.assertEqual(image3.height(), 3)
578+
self.assertEqual(image3.get(0, 0), image.get(4, 6))
579+
self.assertEqual(image3.get(1, 2), image.get(5, 8))
580+
581+
image.write(filename, background='#ff0000')
582+
image4 = tkinter.PhotoImage('::img::test4', master=self.root,
583+
format='ppm', file=filename)
584+
self.assertEqual(image4.get(0, 0), (255, 0, 0))
585+
self.assertEqual(image4.get(4, 6), image.get(4, 6))
586+
587+
image.write(filename, grayscale=True)
588+
image5 = tkinter.PhotoImage('::img::test5', master=self.root,
589+
format='ppm', file=filename)
590+
c = image5.get(4, 6)
591+
self.assertTrue(c[0] == c[1] == c[2], c)
592+
593+
def test_data(self):
594+
image = self.create()
595+
596+
data = image.data()
597+
self.assertIsInstance(data, tuple)
598+
for row in data:
599+
self.assertIsInstance(row, str)
600+
self.assertEqual(data[6].split()[4], '#%02x%02x%02x' % image.get(4, 6))
601+
602+
data = image.data('ppm')
603+
image2 = tkinter.PhotoImage('::img::test2', master=self.root,
604+
format='ppm', data=data)
605+
self.assertEqual(str(image2), '::img::test2')
606+
self.assertEqual(image2.type(), 'photo')
607+
self.assertEqual(image2.width(), 16)
608+
self.assertEqual(image2.height(), 16)
609+
self.assertEqual(image2.get(0, 0), image.get(0, 0))
610+
self.assertEqual(image2.get(4, 6), image.get(4, 6))
611+
612+
data = image.data(format='gif', from_coords=(4, 6, 6, 9))
613+
image3 = tkinter.PhotoImage('::img::test3', master=self.root,
614+
format='gif', data=data)
532615
self.assertEqual(str(image3), '::img::test3')
533616
self.assertEqual(image3.type(), 'photo')
534617
self.assertEqual(image3.width(), 2)
535618
self.assertEqual(image3.height(), 3)
536619
self.assertEqual(image3.get(0, 0), image.get(4, 6))
537620
self.assertEqual(image3.get(1, 2), image.get(5, 8))
538621

622+
data = image.data('ppm', background='#ff0000')
623+
image4 = tkinter.PhotoImage('::img::test4', master=self.root,
624+
format='ppm', data=data)
625+
self.assertEqual(image4.get(0, 0), (255, 0, 0))
626+
self.assertEqual(image4.get(4, 6), image.get(4, 6))
627+
628+
data = image.data('ppm', grayscale=True)
629+
image5 = tkinter.PhotoImage('::img::test5', master=self.root,
630+
format='ppm', data=data)
631+
c = image5.get(4, 6)
632+
self.assertTrue(c[0] == c[1] == c[2], c)
633+
634+
539635
def test_transparency(self):
540636
image = self.create()
541637
self.assertEqual(image.transparency_get(0, 0), True)

Lib/tkinter/__init__.py

+111-11
Original file line numberDiff line numberDiff line change
@@ -4398,17 +4398,117 @@ def put(self, data, to=None):
43984398
to = to[1:]
43994399
args = args + ('-to',) + tuple(to)
44004400
self.tk.call(args)
4401-
# XXX read
4402-
4403-
def write(self, filename, format=None, from_coords=None):
4404-
"""Write image to file FILENAME in FORMAT starting from
4405-
position FROM_COORDS."""
4406-
args = (self.name, 'write', filename)
4407-
if format:
4408-
args = args + ('-format', format)
4409-
if from_coords:
4410-
args = args + ('-from',) + tuple(from_coords)
4411-
self.tk.call(args)
4401+
4402+
def read(self, filename, format=None, *, from_coords=None, to=None, shrink=False):
4403+
"""Reads image data from the file named FILENAME into the image.
4404+
4405+
The FORMAT option specifies the format of the image data in the
4406+
file.
4407+
4408+
The FROM_COORDS option specifies a rectangular sub-region of the image
4409+
file data to be copied to the destination image. It must be a tuple
4410+
or a list of 1 to 4 integers (x1, y1, x2, y2). (x1, y1) and
4411+
(x2, y2) specify diagonally opposite corners of the rectangle. If
4412+
x2 and y2 are not specified, the default value is the bottom-right
4413+
corner of the source image. The default, if this option is not
4414+
specified, is the whole of the image in the image file.
4415+
4416+
The TO option specifies the coordinates of the top-left corner of
4417+
the region of the image into which data from filename are to be
4418+
read. The default is (0, 0).
4419+
4420+
If SHRINK is true, the size of the destination image will be
4421+
reduced, if necessary, so that the region into which the image file
4422+
data are read is at the bottom-right corner of the image.
4423+
"""
4424+
options = ()
4425+
if format is not None:
4426+
options += ('-format', format)
4427+
if from_coords is not None:
4428+
options += ('-from', *from_coords)
4429+
if shrink:
4430+
options += ('-shrink',)
4431+
if to is not None:
4432+
options += ('-to', *to)
4433+
self.tk.call(self.name, 'read', filename, *options)
4434+
4435+
def write(self, filename, format=None, from_coords=None, *,
4436+
background=None, grayscale=False):
4437+
"""Writes image data from the image to a file named FILENAME.
4438+
4439+
The FORMAT option specifies the name of the image file format
4440+
handler to be used to write the data to the file. If this option
4441+
is not given, the format is guessed from the file extension.
4442+
4443+
The FROM_COORDS option specifies a rectangular region of the image
4444+
to be written to the image file. It must be a tuple or a list of 1
4445+
to 4 integers (x1, y1, x2, y2). If only x1 and y1 are specified,
4446+
the region extends from (x1,y1) to the bottom-right corner of the
4447+
image. If all four coordinates are given, they specify diagonally
4448+
opposite corners of the rectangular region. The default, if this
4449+
option is not given, is the whole image.
4450+
4451+
If BACKGROUND is specified, the data will not contain any
4452+
transparency information. In all transparent pixels the color will
4453+
be replaced by the specified color.
4454+
4455+
If GRAYSCALE is true, the data will not contain color information.
4456+
All pixel data will be transformed into grayscale.
4457+
"""
4458+
options = ()
4459+
if format is not None:
4460+
options += ('-format', format)
4461+
if from_coords is not None:
4462+
options += ('-from', *from_coords)
4463+
if grayscale:
4464+
options += ('-grayscale',)
4465+
if background is not None:
4466+
options += ('-background', background)
4467+
self.tk.call(self.name, 'write', filename, *options)
4468+
4469+
def data(self, format=None, *, from_coords=None,
4470+
background=None, grayscale=False):
4471+
"""Returns image data.
4472+
4473+
The FORMAT option specifies the name of the image file format
4474+
handler to be used. If this option is not given, this method uses
4475+
a format that consists of a tuple (one element per row) of strings
4476+
containings space separated (one element per pixel/column) colors
4477+
in “#RRGGBB” format (where RR is a pair of hexadecimal digits for
4478+
the red channel, GG for green, and BB for blue).
4479+
4480+
The FROM_COORDS option specifies a rectangular region of the image
4481+
to be returned. It must be a tuple or a list of 1 to 4 integers
4482+
(x1, y1, x2, y2). If only x1 and y1 are specified, the region
4483+
extends from (x1,y1) to the bottom-right corner of the image. If
4484+
all four coordinates are given, they specify diagonally opposite
4485+
corners of the rectangular region, including (x1, y1) and excluding
4486+
(x2, y2). The default, if this option is not given, is the whole
4487+
image.
4488+
4489+
If BACKGROUND is specified, the data will not contain any
4490+
transparency information. In all transparent pixels the color will
4491+
be replaced by the specified color.
4492+
4493+
If GRAYSCALE is true, the data will not contain color information.
4494+
All pixel data will be transformed into grayscale.
4495+
"""
4496+
options = ()
4497+
if format is not None:
4498+
options += ('-format', format)
4499+
if from_coords is not None:
4500+
options += ('-from', *from_coords)
4501+
if grayscale:
4502+
options += ('-grayscale',)
4503+
if background is not None:
4504+
options += ('-background', background)
4505+
data = self.tk.call(self.name, 'data', *options)
4506+
if isinstance(data, str): # For wantobjects = 0.
4507+
if format is None:
4508+
data = self.tk.splitlist(data)
4509+
else:
4510+
data = bytes(data, 'latin1')
4511+
return data
44124512

44134513
def transparency_get(self, x, y):
44144514
"""Return True if the pixel at x,y is transparent."""
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Add the :class:`!PhotoImage` methods :meth:`~tkinter.PhotoImage.read` to
2+
read an image from a file and :meth:`~tkinter.PhotoImage.data` to get the
3+
image data. Add *background* and *grayscale* parameters to
4+
:class:`!PhotoImage` method :meth:`~tkinter.PhotoImage.write`.

0 commit comments

Comments
 (0)