Skip to content

Commit 432a235

Browse files
gh-118271: Support more options for reading/writing images in Tkinter
* 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 7d369d4 commit 432a235

File tree

4 files changed

+222
-16
lines changed

4 files changed

+222
-16
lines changed

Doc/whatsnew/3.13.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -791,6 +791,12 @@ tkinter
791791
:class:`tkinter.ttk.Style`.
792792
(Contributed by Serhiy Storchaka in :gh:`68166`.)
793793

794+
* Add the :class:`!PhotoImage` methods :meth:`!read` to read
795+
an image from a file and :meth:`!data` to get the image data.
796+
Add *background* and *grayscale* parameters to :class:`!PhotoImage` method
797+
:meth:`!write`.
798+
(Contributed by Serhiy Storchaka in :gh:`118271`.)
799+
794800
traceback
795801
---------
796802

Lib/test/test_tkinter/test_images.py

Lines changed: 101 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -356,6 +356,50 @@ def test_get(self):
356356
self.assertRaises(tkinter.TclError, image.get, 16, 15)
357357
self.assertRaises(tkinter.TclError, image.get, 15, 16)
358358

359+
def test_read(self):
360+
# Due to the Tk bug https://core.tcl-lang.org/tk/tktview/1576528
361+
# the -from option does not work correctly for GIF and PNG files.
362+
# Use the PPM file for this test.
363+
testfile = support.findfile('python.ppm', subdir='tkinterdata')
364+
image = tkinter.PhotoImage(master=self.root, file=testfile)
365+
366+
image2 = tkinter.PhotoImage(master=self.root)
367+
image2.read(testfile)
368+
self.assertEqual(image2.type(), 'photo')
369+
self.assertEqual(image2.width(), 16)
370+
self.assertEqual(image2.height(), 16)
371+
self.assertEqual(image2.get(0, 0), image.get(0, 0))
372+
self.assertEqual(image2.get(4, 6), image.get(4, 6))
373+
374+
self.assertRaises(tkinter.TclError, image2.read, self.testfile, 'ppm')
375+
376+
image2 = tkinter.PhotoImage(master=self.root)
377+
image2.read(testfile, from_=(2, 3, 14, 11))
378+
self.assertEqual(image2.width(), 12)
379+
self.assertEqual(image2.height(), 8)
380+
self.assertEqual(image2.get(0, 0), image.get(2, 3))
381+
self.assertEqual(image2.get(11, 7), image.get(13, 10))
382+
self.assertEqual(image2.get(2, 4), image.get(2+2, 4+3))
383+
384+
image2 = tkinter.PhotoImage(master=self.root, file=testfile)
385+
self.assertEqual(image2.width(), 16)
386+
self.assertEqual(image2.height(), 16)
387+
image2.read(testfile, from_=(2, 3, 14, 11), shrink=True)
388+
self.assertEqual(image2.width(), 12)
389+
self.assertEqual(image2.height(), 8)
390+
self.assertEqual(image2.get(0, 0), image.get(2, 3))
391+
self.assertEqual(image2.get(11, 7), image.get(13, 10))
392+
self.assertEqual(image2.get(2, 4), image.get(2+2, 4+3))
393+
394+
image2 = tkinter.PhotoImage(master=self.root)
395+
image2.read(testfile, from_=(2, 3, 14, 11), to=(3, 6))
396+
self.assertEqual(image2.type(), 'photo')
397+
self.assertEqual(image2.width(), 15)
398+
self.assertEqual(image2.height(), 14)
399+
self.assertEqual(image2.get(0+3, 0+6), image.get(2, 3))
400+
self.assertEqual(image2.get(11+3, 7+6), image.get(13, 10))
401+
self.assertEqual(image2.get(2+3, 4+6), image.get(2+2, 4+3))
402+
359403
def test_write(self):
360404
filename = os_helper.TESTFN
361405
import locale
@@ -367,26 +411,78 @@ def test_write(self):
367411

368412
image.write(filename)
369413
image2 = tkinter.PhotoImage('::img::test2', master=self.root,
370-
format='ppm',
371-
file=filename)
414+
format='ppm', file=filename)
372415
self.assertEqual(str(image2), '::img::test2')
373416
self.assertEqual(image2.type(), 'photo')
374417
self.assertEqual(image2.width(), 16)
375418
self.assertEqual(image2.height(), 16)
376419
self.assertEqual(image2.get(0, 0), image.get(0, 0))
377-
self.assertEqual(image2.get(15, 8), image.get(15, 8))
420+
self.assertEqual(image2.get(4, 6), image.get(4, 6))
378421

379422
image.write(filename, format='gif', from_coords=(4, 6, 6, 9))
380423
image3 = tkinter.PhotoImage('::img::test3', master=self.root,
381-
format='gif',
382-
file=filename)
424+
format='gif', file=filename)
383425
self.assertEqual(str(image3), '::img::test3')
384426
self.assertEqual(image3.type(), 'photo')
385427
self.assertEqual(image3.width(), 2)
386428
self.assertEqual(image3.height(), 3)
387429
self.assertEqual(image3.get(0, 0), image.get(4, 6))
388430
self.assertEqual(image3.get(1, 2), image.get(5, 8))
389431

432+
image.write(filename, background='#ff0000')
433+
image4 = tkinter.PhotoImage('::img::test4', master=self.root,
434+
format='ppm', file=filename)
435+
self.assertEqual(image4.get(0, 0), (255, 0, 0))
436+
self.assertEqual(image4.get(4, 6), image.get(4, 6))
437+
438+
image.write(filename, grayscale=True)
439+
image5 = tkinter.PhotoImage('::img::test5', master=self.root,
440+
format='ppm', file=filename)
441+
c = image5.get(4, 6)
442+
self.assertTrue(c[0] == c[1] == c[2], c)
443+
444+
def test_data(self):
445+
image = self.create()
446+
447+
data = image.data()
448+
self.assertIsInstance(data, tuple)
449+
for row in data:
450+
self.assertIsInstance(row, str)
451+
self.assertEqual(data[6].split()[4], '#%02x%02x%02x' % image.get(4, 6))
452+
453+
data = image.data('ppm')
454+
image2 = tkinter.PhotoImage('::img::test2', master=self.root,
455+
format='ppm', data=data)
456+
self.assertEqual(str(image2), '::img::test2')
457+
self.assertEqual(image2.type(), 'photo')
458+
self.assertEqual(image2.width(), 16)
459+
self.assertEqual(image2.height(), 16)
460+
self.assertEqual(image2.get(0, 0), image.get(0, 0))
461+
self.assertEqual(image2.get(4, 6), image.get(4, 6))
462+
463+
data = image.data(format='gif', from_=(4, 6, 6, 9))
464+
image3 = tkinter.PhotoImage('::img::test3', master=self.root,
465+
format='gif', data=data)
466+
self.assertEqual(str(image3), '::img::test3')
467+
self.assertEqual(image3.type(), 'photo')
468+
self.assertEqual(image3.width(), 2)
469+
self.assertEqual(image3.height(), 3)
470+
self.assertEqual(image3.get(0, 0), image.get(4, 6))
471+
self.assertEqual(image3.get(1, 2), image.get(5, 8))
472+
473+
data = image.data('ppm', background='#ff0000')
474+
image4 = tkinter.PhotoImage('::img::test4', master=self.root,
475+
format='ppm', data=data)
476+
self.assertEqual(image4.get(0, 0), (255, 0, 0))
477+
self.assertEqual(image4.get(4, 6), image.get(4, 6))
478+
479+
data = image.data('ppm', grayscale=True)
480+
image5 = tkinter.PhotoImage('::img::test5', master=self.root,
481+
format='ppm', data=data)
482+
c = image5.get(4, 6)
483+
self.assertTrue(c[0] == c[1] == c[2], c)
484+
485+
390486
def test_transparency(self):
391487
image = self.create()
392488
self.assertEqual(image.transparency_get(0, 0), True)

Lib/tkinter/__init__.py

Lines changed: 111 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4304,17 +4304,117 @@ def put(self, data, to=None):
43044304
to = to[1:]
43054305
args = args + ('-to',) + tuple(to)
43064306
self.tk.call(args)
4307-
# XXX read
4308-
4309-
def write(self, filename, format=None, from_coords=None):
4310-
"""Write image to file FILENAME in FORMAT starting from
4311-
position FROM_COORDS."""
4312-
args = (self.name, 'write', filename)
4313-
if format:
4314-
args = args + ('-format', format)
4315-
if from_coords:
4316-
args = args + ('-from',) + tuple(from_coords)
4317-
self.tk.call(args)
4307+
4308+
def read(self, filename, format=None, *, from_=None, to=None, shrink=False):
4309+
"""Reads image data from the file named FILENAME into the image.
4310+
4311+
The FORMAT option specifies the format of the image data in the
4312+
file.
4313+
4314+
The FROM_ option specifies a rectangular sub-region of the image
4315+
file data to be copied to the destination image. It must be a tuple
4316+
or a list of 1 to 4 integers (x1, y1, x2, y2). (x1, y1) and
4317+
(x2, y2) specify diagonally opposite corners of the rectangle. If
4318+
x2 and y2 are not specified, the default value is the bottom-right
4319+
corner of the source image. The default, if this option is not
4320+
specified, is the whole of the image in the image file.
4321+
4322+
The TO option specifies the coordinates of the top-left corner of
4323+
the region of the image into which data from filename are to be
4324+
read. The default is (0, 0).
4325+
4326+
If SHRINK is true, the size of the destination image will be
4327+
reduced, if necessary, so that the region into which the image file
4328+
data are read is at the bottom-right corner of the image.
4329+
"""
4330+
options = ()
4331+
if format is not None:
4332+
options += ('-format', format)
4333+
if from_ is not None:
4334+
options += ('-from', *from_)
4335+
if shrink:
4336+
options += ('-shrink',)
4337+
if to is not None:
4338+
options += ('-to', *to)
4339+
self.tk.call(self.name, 'read', filename, *options)
4340+
4341+
def write(self, filename, format=None, from_coords=None, *,
4342+
background=None, grayscale=False):
4343+
"""Writes image data from the image to a file named FILENAME.
4344+
4345+
The FORMAT option specifies the name of the image file format
4346+
handler to be used to write the data to the file. If this option
4347+
is not given, the format is guessed from the file extension.
4348+
4349+
The FROM_COORDS option specifies a rectangular region of the image
4350+
to be written to the image file. It must be a tuple or a list of 1
4351+
to 4 integers (x1, y1, x2, y2). If only x1 and y1 are specified,
4352+
the region extends from (x1,y1) to the bottom-right corner of the
4353+
image. If all four coordinates are given, they specify diagonally
4354+
opposite corners of the rectangular region. The default, if this
4355+
option is not given, is the whole image.
4356+
4357+
If BACKGROUND is specified, the data will not contain any
4358+
transparency information. In all transparent pixels the color will
4359+
be replaced by the specified color.
4360+
4361+
If GRAYSCALE is true, the data will not contain color information.
4362+
All pixel data will be transformed into grayscale.
4363+
"""
4364+
options = ()
4365+
if format is not None:
4366+
options += ('-format', format)
4367+
if from_coords is not None:
4368+
options += ('-from', *from_coords)
4369+
if grayscale:
4370+
options += ('-grayscale',)
4371+
if background is not None:
4372+
options += ('-background', background)
4373+
self.tk.call(self.name, 'write', filename, *options)
4374+
4375+
def data(self, format=None, *, from_=None,
4376+
background=None, grayscale=False):
4377+
"""Returns image data.
4378+
4379+
The FORMAT option specifies the name of the image file format
4380+
handler to be used. If this option is not given, this method uses
4381+
a format that consists of a tuple (one element per row) of strings
4382+
containings space separated (one element per pixel/column) colors
4383+
in “#RRGGBB” format (where RR is a pair of hexadecimal digits for
4384+
the red channel, GG for green, and BB for blue).
4385+
4386+
The FROM_ option specifies a rectangular region of the image
4387+
to be returned. It must be a tuple or a list of 1 to 4 integers
4388+
(x1, y1, x2, y2). If only x1 and y1 are specified, the region
4389+
extends from (x1,y1) to the bottom-right corner of the image. If
4390+
all four coordinates are given, they specify diagonally opposite
4391+
corners of the rectangular region, including (x1, y1) and excluding
4392+
(x2, y2). The default, if this option is not given, is the whole
4393+
image.
4394+
4395+
If BACKGROUND is specified, the data will not contain any
4396+
transparency information. In all transparent pixels the color will
4397+
be replaced by the specified color.
4398+
4399+
If GRAYSCALE is true, the data will not contain color information.
4400+
All pixel data will be transformed into grayscale.
4401+
"""
4402+
options = ()
4403+
if format is not None:
4404+
optionsoptions += ('-format', format)
4405+
if from_ is not None:
4406+
optionsoptions += ('-from', *from_)
4407+
if grayscale:
4408+
optionsoptions += ('-grayscale',)
4409+
if background is not None:
4410+
optionsoptions += ('-background', background)
4411+
data = self.tk.call(self.name, 'data', *options)
4412+
if isinstance(data, str): # For wantobjects = 0.
4413+
if format is None:
4414+
data = self.tk.splitlist(data)
4415+
else:
4416+
data = bytes(data, 'latin1')
4417+
return data
43184418

43194419
def transparency_get(self, x, y):
43204420
"""Return True if the pixel at x,y is transparent."""
Lines changed: 4 additions & 0 deletions
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)