mirror of
https://github.com/python/cpython.git
synced 2025-11-03 11:23:31 +00:00
gh-118225: Support more options for copying images in Tkinter (GH-118228)
* Add the PhotoImage method copy_replace() to copy a region from one image to other image, possibly with pixel zooming and/or subsampling. * Add from_coords parameter to PhotoImage methods copy(), zoom() and subsample(). * Add zoom and subsample parameters to PhotoImage method copy().
This commit is contained in:
parent
09871c9223
commit
1b639a04ca
5 changed files with 271 additions and 20 deletions
|
|
@ -979,6 +979,15 @@ of :class:`tkinter.Image`:
|
||||||
Either type of image is created through either the ``file`` or the ``data``
|
Either type of image is created through either the ``file`` or the ``data``
|
||||||
option (other options are available as well).
|
option (other options are available as well).
|
||||||
|
|
||||||
|
.. versionchanged:: 3.13
|
||||||
|
Added the :class:`!PhotoImage` method :meth:`!copy_replace` to copy a region
|
||||||
|
from one image to other image, possibly with pixel zooming and/or
|
||||||
|
subsampling.
|
||||||
|
Add *from_coords* parameter to :class:`!PhotoImage` methods :meth:`!copy()`,
|
||||||
|
:meth:`!zoom()` and :meth:`!subsample()`.
|
||||||
|
Add *zoom* and *subsample* parameters to :class:`!PhotoImage` method
|
||||||
|
:meth:`!copy()`.
|
||||||
|
|
||||||
The image object can then be used wherever an ``image`` option is supported by
|
The image object can then be used wherever an ``image`` option is supported by
|
||||||
some widget (e.g. labels, buttons, menus). In these cases, Tk will not keep a
|
some widget (e.g. labels, buttons, menus). In these cases, Tk will not keep a
|
||||||
reference to the image. When the last Python reference to the image object is
|
reference to the image. When the last Python reference to the image object is
|
||||||
|
|
|
||||||
|
|
@ -882,6 +882,15 @@ tkinter
|
||||||
* Add the :meth:`!after_info` method for Tkinter widgets.
|
* Add the :meth:`!after_info` method for Tkinter widgets.
|
||||||
(Contributed by Cheryl Sabella in :gh:`77020`.)
|
(Contributed by Cheryl Sabella in :gh:`77020`.)
|
||||||
|
|
||||||
|
* Add the :class:`!PhotoImage` method :meth:`!copy_replace` to copy a region
|
||||||
|
from one image to other image, possibly with pixel zooming and/or
|
||||||
|
subsampling.
|
||||||
|
Add *from_coords* parameter to :class:`!PhotoImage` methods :meth:`!copy()`,
|
||||||
|
:meth:`!zoom()` and :meth:`!subsample()`.
|
||||||
|
Add *zoom* and *subsample* parameters to :class:`!PhotoImage` method
|
||||||
|
:meth:`!copy()`.
|
||||||
|
(Contributed by Serhiy Storchaka in :gh:`118225`.)
|
||||||
|
|
||||||
traceback
|
traceback
|
||||||
---------
|
---------
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -302,7 +302,37 @@ class PhotoImageTest(AbstractTkTest, unittest.TestCase):
|
||||||
image2 = image.copy()
|
image2 = image.copy()
|
||||||
self.assertEqual(image2.width(), 16)
|
self.assertEqual(image2.width(), 16)
|
||||||
self.assertEqual(image2.height(), 16)
|
self.assertEqual(image2.height(), 16)
|
||||||
self.assertEqual(image.get(4, 6), image.get(4, 6))
|
self.assertEqual(image2.get(4, 6), image.get(4, 6))
|
||||||
|
|
||||||
|
image2 = image.copy(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 = image.copy(from_coords=(2, 3, 14, 11), zoom=2)
|
||||||
|
self.assertEqual(image2.width(), 24)
|
||||||
|
self.assertEqual(image2.height(), 16)
|
||||||
|
self.assertEqual(image2.get(0, 0), image.get(2, 3))
|
||||||
|
self.assertEqual(image2.get(23, 15), image.get(13, 10))
|
||||||
|
self.assertEqual(image2.get(2*2, 4*2), image.get(2+2, 4+3))
|
||||||
|
self.assertEqual(image2.get(2*2+1, 4*2+1), image.get(6+2, 2+3))
|
||||||
|
|
||||||
|
image2 = image.copy(from_coords=(2, 3, 14, 11), subsample=2)
|
||||||
|
self.assertEqual(image2.width(), 6)
|
||||||
|
self.assertEqual(image2.height(), 4)
|
||||||
|
self.assertEqual(image2.get(0, 0), image.get(2, 3))
|
||||||
|
self.assertEqual(image2.get(5, 3), image.get(12, 9))
|
||||||
|
self.assertEqual(image2.get(3, 2), image.get(3*2+2, 2*2+3))
|
||||||
|
|
||||||
|
image2 = image.copy(from_coords=(2, 3, 14, 11), subsample=2, zoom=3)
|
||||||
|
self.assertEqual(image2.width(), 18)
|
||||||
|
self.assertEqual(image2.height(), 12)
|
||||||
|
self.assertEqual(image2.get(0, 0), image.get(2, 3))
|
||||||
|
self.assertEqual(image2.get(17, 11), image.get(12, 9))
|
||||||
|
self.assertEqual(image2.get(1*3, 2*3), image.get(1*2+2, 2*2+3))
|
||||||
|
self.assertEqual(image2.get(1*3+2, 2*3+2), image.get(1*2+2, 2*2+3))
|
||||||
|
|
||||||
def test_subsample(self):
|
def test_subsample(self):
|
||||||
image = self.create()
|
image = self.create()
|
||||||
|
|
@ -316,6 +346,13 @@ class PhotoImageTest(AbstractTkTest, unittest.TestCase):
|
||||||
self.assertEqual(image2.height(), 8)
|
self.assertEqual(image2.height(), 8)
|
||||||
self.assertEqual(image2.get(2, 3), image.get(4, 6))
|
self.assertEqual(image2.get(2, 3), image.get(4, 6))
|
||||||
|
|
||||||
|
image2 = image.subsample(2, from_coords=(2, 3, 14, 11))
|
||||||
|
self.assertEqual(image2.width(), 6)
|
||||||
|
self.assertEqual(image2.height(), 4)
|
||||||
|
self.assertEqual(image2.get(0, 0), image.get(2, 3))
|
||||||
|
self.assertEqual(image2.get(5, 3), image.get(12, 9))
|
||||||
|
self.assertEqual(image2.get(1, 2), image.get(1*2+2, 2*2+3))
|
||||||
|
|
||||||
def test_zoom(self):
|
def test_zoom(self):
|
||||||
image = self.create()
|
image = self.create()
|
||||||
image2 = image.zoom(2, 3)
|
image2 = image.zoom(2, 3)
|
||||||
|
|
@ -330,6 +367,118 @@ class PhotoImageTest(AbstractTkTest, unittest.TestCase):
|
||||||
self.assertEqual(image2.get(8, 12), image.get(4, 6))
|
self.assertEqual(image2.get(8, 12), image.get(4, 6))
|
||||||
self.assertEqual(image2.get(9, 13), image.get(4, 6))
|
self.assertEqual(image2.get(9, 13), image.get(4, 6))
|
||||||
|
|
||||||
|
image2 = image.zoom(2, from_coords=(2, 3, 14, 11))
|
||||||
|
self.assertEqual(image2.width(), 24)
|
||||||
|
self.assertEqual(image2.height(), 16)
|
||||||
|
self.assertEqual(image2.get(0, 0), image.get(2, 3))
|
||||||
|
self.assertEqual(image2.get(23, 15), image.get(13, 10))
|
||||||
|
self.assertEqual(image2.get(2*2, 4*2), image.get(2+2, 4+3))
|
||||||
|
self.assertEqual(image2.get(2*2+1, 4*2+1), image.get(6+2, 2+3))
|
||||||
|
|
||||||
|
def test_copy_replace(self):
|
||||||
|
image = self.create()
|
||||||
|
image2 = tkinter.PhotoImage(master=self.root)
|
||||||
|
image2.copy_replace(image)
|
||||||
|
self.assertEqual(image2.width(), 16)
|
||||||
|
self.assertEqual(image2.height(), 16)
|
||||||
|
self.assertEqual(image2.get(4, 6), image.get(4, 6))
|
||||||
|
|
||||||
|
image2 = tkinter.PhotoImage(master=self.root)
|
||||||
|
image2.copy_replace(image, 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)
|
||||||
|
image2.copy_replace(image)
|
||||||
|
image2.copy_replace(image, 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.copy_replace(image, from_coords=(2, 3, 14, 11), to=(3, 6))
|
||||||
|
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))
|
||||||
|
|
||||||
|
image2 = tkinter.PhotoImage(master=self.root)
|
||||||
|
image2.copy_replace(image, from_coords=(2, 3, 14, 11), to=(0, 0, 100, 50))
|
||||||
|
self.assertEqual(image2.width(), 100)
|
||||||
|
self.assertEqual(image2.height(), 50)
|
||||||
|
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))
|
||||||
|
self.assertEqual(image2.get(2+12, 4+8), image.get(2+2, 4+3))
|
||||||
|
self.assertEqual(image2.get(2+12*2, 4), image.get(2+2, 4+3))
|
||||||
|
self.assertEqual(image2.get(2, 4+8*3), image.get(2+2, 4+3))
|
||||||
|
|
||||||
|
image2 = tkinter.PhotoImage(master=self.root)
|
||||||
|
image2.copy_replace(image, from_coords=(2, 3, 14, 11), zoom=2)
|
||||||
|
self.assertEqual(image2.width(), 24)
|
||||||
|
self.assertEqual(image2.height(), 16)
|
||||||
|
self.assertEqual(image2.get(0, 0), image.get(2, 3))
|
||||||
|
self.assertEqual(image2.get(23, 15), image.get(13, 10))
|
||||||
|
self.assertEqual(image2.get(2*2, 4*2), image.get(2+2, 4+3))
|
||||||
|
self.assertEqual(image2.get(2*2+1, 4*2+1), image.get(6+2, 2+3))
|
||||||
|
|
||||||
|
image2 = tkinter.PhotoImage(master=self.root)
|
||||||
|
image2.copy_replace(image, from_coords=(2, 3, 14, 11), subsample=2)
|
||||||
|
self.assertEqual(image2.width(), 6)
|
||||||
|
self.assertEqual(image2.height(), 4)
|
||||||
|
self.assertEqual(image2.get(0, 0), image.get(2, 3))
|
||||||
|
self.assertEqual(image2.get(5, 3), image.get(12, 9))
|
||||||
|
self.assertEqual(image2.get(1, 2), image.get(1*2+2, 2*2+3))
|
||||||
|
|
||||||
|
image2 = tkinter.PhotoImage(master=self.root)
|
||||||
|
image2.copy_replace(image, from_coords=(2, 3, 14, 11), subsample=2, zoom=3)
|
||||||
|
self.assertEqual(image2.width(), 18)
|
||||||
|
self.assertEqual(image2.height(), 12)
|
||||||
|
self.assertEqual(image2.get(0, 0), image.get(2, 3))
|
||||||
|
self.assertEqual(image2.get(17, 11), image.get(12, 9))
|
||||||
|
self.assertEqual(image2.get(3*3, 2*3), image.get(3*2+2, 2*2+3))
|
||||||
|
self.assertEqual(image2.get(3*3+2, 2*3+2), image.get(3*2+2, 2*2+3))
|
||||||
|
self.assertEqual(image2.get(1*3, 2*3), image.get(1*2+2, 2*2+3))
|
||||||
|
self.assertEqual(image2.get(1*3+2, 2*3+2), image.get(1*2+2, 2*2+3))
|
||||||
|
|
||||||
|
def checkImgTrans(self, image, expected):
|
||||||
|
actual = {(x, y)
|
||||||
|
for x in range(image.width())
|
||||||
|
for y in range(image.height())
|
||||||
|
if image.transparency_get(x, y)}
|
||||||
|
self.assertEqual(actual, expected)
|
||||||
|
|
||||||
|
def test_copy_replace_compositingrule(self):
|
||||||
|
image1 = tkinter.PhotoImage(master=self.root, width=2, height=2)
|
||||||
|
image1.blank()
|
||||||
|
image1.put('black', to=(0, 0, 2, 2))
|
||||||
|
image1.transparency_set(0, 0, True)
|
||||||
|
|
||||||
|
# default compositingrule
|
||||||
|
image2 = tkinter.PhotoImage(master=self.root, width=3, height=3)
|
||||||
|
image2.blank()
|
||||||
|
image2.put('white', to=(0, 0, 2, 2))
|
||||||
|
image2.copy_replace(image1, to=(1, 1))
|
||||||
|
self.checkImgTrans(image2, {(0, 2), (2, 0)})
|
||||||
|
|
||||||
|
image3 = tkinter.PhotoImage(master=self.root, width=3, height=3)
|
||||||
|
image3.blank()
|
||||||
|
image3.put('white', to=(0, 0, 2, 2))
|
||||||
|
image3.copy_replace(image1, to=(1, 1), compositingrule='overlay')
|
||||||
|
self.checkImgTrans(image3, {(0, 2), (2, 0)})
|
||||||
|
|
||||||
|
image4 = tkinter.PhotoImage(master=self.root, width=3, height=3)
|
||||||
|
image4.blank()
|
||||||
|
image4.put('white', to=(0, 0, 2, 2))
|
||||||
|
image4.copy_replace(image1, to=(1, 1), compositingrule='set')
|
||||||
|
self.checkImgTrans(image4, {(0, 2), (1, 1), (2, 0)})
|
||||||
|
|
||||||
def test_put(self):
|
def test_put(self):
|
||||||
image = self.create()
|
image = self.create()
|
||||||
image.put('{red green} {blue yellow}', to=(4, 6))
|
image.put('{red green} {blue yellow}', to=(4, 6))
|
||||||
|
|
|
||||||
|
|
@ -4278,33 +4278,112 @@ class PhotoImage(Image):
|
||||||
|
|
||||||
def __getitem__(self, key):
|
def __getitem__(self, key):
|
||||||
return self.tk.call(self.name, 'cget', '-' + key)
|
return self.tk.call(self.name, 'cget', '-' + key)
|
||||||
# XXX copy -from, -to, ...?
|
|
||||||
|
|
||||||
def copy(self):
|
def copy(self, *, from_coords=None, zoom=None, subsample=None):
|
||||||
"""Return a new PhotoImage with the same image as this widget."""
|
"""Return a new PhotoImage with the same image as this widget.
|
||||||
|
|
||||||
|
The FROM_COORDS option specifies a rectangular sub-region of the
|
||||||
|
source image to be copied. 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 pixels copied will include the left and top edges of the
|
||||||
|
specified rectangle but not the bottom or right edges. If the
|
||||||
|
FROM_COORDS option is not given, the default is the whole source
|
||||||
|
image.
|
||||||
|
|
||||||
|
If SUBSAMPLE or ZOOM are specified, the image is transformed as in
|
||||||
|
the subsample() or zoom() methods. The value must be a single
|
||||||
|
integer or a pair of integers.
|
||||||
|
"""
|
||||||
destImage = PhotoImage(master=self.tk)
|
destImage = PhotoImage(master=self.tk)
|
||||||
self.tk.call(destImage, 'copy', self.name)
|
destImage.copy_replace(self, from_coords=from_coords,
|
||||||
|
zoom=zoom, subsample=subsample)
|
||||||
return destImage
|
return destImage
|
||||||
|
|
||||||
def zoom(self, x, y=''):
|
def zoom(self, x, y='', *, from_coords=None):
|
||||||
"""Return a new PhotoImage with the same image as this widget
|
"""Return a new PhotoImage with the same image as this widget
|
||||||
but zoom it with a factor of x in the X direction and y in the Y
|
but zoom it with a factor of X in the X direction and Y in the Y
|
||||||
direction. If y is not given, the default value is the same as x.
|
direction. If Y is not given, the default value is the same as X.
|
||||||
"""
|
|
||||||
destImage = PhotoImage(master=self.tk)
|
|
||||||
if y=='': y=x
|
|
||||||
self.tk.call(destImage, 'copy', self.name, '-zoom',x,y)
|
|
||||||
return destImage
|
|
||||||
|
|
||||||
def subsample(self, x, y=''):
|
The FROM_COORDS option specifies a rectangular sub-region of the
|
||||||
"""Return a new PhotoImage based on the same image as this widget
|
source image to be copied, as in the copy() method.
|
||||||
but use only every Xth or Yth pixel. If y is not given, the
|
|
||||||
default value is the same as x.
|
|
||||||
"""
|
"""
|
||||||
destImage = PhotoImage(master=self.tk)
|
|
||||||
if y=='': y=x
|
if y=='': y=x
|
||||||
self.tk.call(destImage, 'copy', self.name, '-subsample',x,y)
|
return self.copy(zoom=(x, y), from_coords=from_coords)
|
||||||
return destImage
|
|
||||||
|
def subsample(self, x, y='', *, from_coords=None):
|
||||||
|
"""Return a new PhotoImage based on the same image as this widget
|
||||||
|
but use only every Xth or Yth pixel. If Y is not given, the
|
||||||
|
default value is the same as X.
|
||||||
|
|
||||||
|
The FROM_COORDS option specifies a rectangular sub-region of the
|
||||||
|
source image to be copied, as in the copy() method.
|
||||||
|
"""
|
||||||
|
if y=='': y=x
|
||||||
|
return self.copy(subsample=(x, y), from_coords=from_coords)
|
||||||
|
|
||||||
|
def copy_replace(self, sourceImage, *, from_coords=None, to=None, shrink=False,
|
||||||
|
zoom=None, subsample=None, compositingrule=None):
|
||||||
|
"""Copy a region from the source image (which must be a PhotoImage) to
|
||||||
|
this image, possibly with pixel zooming and/or subsampling. If no
|
||||||
|
options are specified, this command copies the whole of the source
|
||||||
|
image into this image, starting at coordinates (0, 0).
|
||||||
|
|
||||||
|
The FROM_COORDS option specifies a rectangular sub-region of the
|
||||||
|
source image to be copied. 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 pixels copied will include the left and top edges of the
|
||||||
|
specified rectangle but not the bottom or right edges. If the
|
||||||
|
FROM_COORDS option is not given, the default is the whole source
|
||||||
|
image.
|
||||||
|
|
||||||
|
The TO option specifies a rectangular sub-region of the destination
|
||||||
|
image to be affected. 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 (x1,y1) plus the size of the source region
|
||||||
|
(after subsampling and zooming, if specified). If x2 and y2 are
|
||||||
|
specified, the source region will be replicated if necessary to fill
|
||||||
|
the destination region in a tiled fashion.
|
||||||
|
|
||||||
|
If SHRINK is true, the size of the destination image should be
|
||||||
|
reduced, if necessary, so that the region being copied into is at
|
||||||
|
the bottom-right corner of the image.
|
||||||
|
|
||||||
|
If SUBSAMPLE or ZOOM are specified, the image is transformed as in
|
||||||
|
the subsample() or zoom() methods. The value must be a single
|
||||||
|
integer or a pair of integers.
|
||||||
|
|
||||||
|
The COMPOSITINGRULE option specifies how transparent pixels in the
|
||||||
|
source image are combined with the destination image. When a
|
||||||
|
compositing rule of 'overlay' is set, the old contents of the
|
||||||
|
destination image are visible, as if the source image were printed
|
||||||
|
on a piece of transparent film and placed over the top of the
|
||||||
|
destination. When a compositing rule of 'set' is set, the old
|
||||||
|
contents of the destination image are discarded and the source image
|
||||||
|
is used as-is. The default compositing rule is 'overlay'.
|
||||||
|
"""
|
||||||
|
options = []
|
||||||
|
if from_coords is not None:
|
||||||
|
options.extend(('-from', *from_coords))
|
||||||
|
if to is not None:
|
||||||
|
options.extend(('-to', *to))
|
||||||
|
if shrink:
|
||||||
|
options.append('-shrink')
|
||||||
|
if zoom is not None:
|
||||||
|
if not isinstance(zoom, (tuple, list)):
|
||||||
|
zoom = (zoom,)
|
||||||
|
options.extend(('-zoom', *zoom))
|
||||||
|
if subsample is not None:
|
||||||
|
if not isinstance(subsample, (tuple, list)):
|
||||||
|
subsample = (subsample,)
|
||||||
|
options.extend(('-subsample', *subsample))
|
||||||
|
if compositingrule:
|
||||||
|
options.extend(('-compositingrule', compositingrule))
|
||||||
|
self.tk.call(self.name, 'copy', sourceImage, *options)
|
||||||
|
|
||||||
def get(self, x, y):
|
def get(self, x, y):
|
||||||
"""Return the color (red, green, blue) of the pixel at X,Y."""
|
"""Return the color (red, green, blue) of the pixel at X,Y."""
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
Add the :class:`!PhotoImage` method :meth:`!copy_replace` to copy a region
|
||||||
|
from one image to other image, possibly with pixel zooming and/or
|
||||||
|
subsampling. Add *from_coords* parameter to :class:`!PhotoImage` methods
|
||||||
|
:meth:`!copy()`, :meth:`!zoom()` and :meth:`!subsample()`. Add *zoom* and
|
||||||
|
*subsample* parameters to :class:`!PhotoImage` method :meth:`!copy()`.
|
||||||
Loading…
Add table
Add a link
Reference in a new issue