From 2ce8164257858908106a1d11ddd5953be6d3be51 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Wed, 24 Apr 2024 16:07:30 +0300 Subject: [PATCH 1/2] gh-118225: Support more options for copying images in Tkinter * 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_ parameter to PhotoImage methods copy(), zoom() and subsample(). * Add zoom and subsample parameters to PhotoImage method copy(). --- Doc/library/tkinter.rst | 9 ++ Doc/whatsnew/3.13.rst | 9 ++ Lib/test/test_tkinter/test_images.py | 151 +++++++++++++++++- Lib/tkinter/__init__.py | 100 ++++++++++-- ...-04-24-16-07-26.gh-issue-118225.KdrcgL.rst | 5 + 5 files changed, 261 insertions(+), 13 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2024-04-24-16-07-26.gh-issue-118225.KdrcgL.rst diff --git a/Doc/library/tkinter.rst b/Doc/library/tkinter.rst index e084d8554c7c09..f7d9c3682c6db4 100644 --- a/Doc/library/tkinter.rst +++ b/Doc/library/tkinter.rst @@ -979,6 +979,15 @@ of :class:`tkinter.Image`: Either type of image is created through either the ``file`` or the ``data`` 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_* 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 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 diff --git a/Doc/whatsnew/3.13.rst b/Doc/whatsnew/3.13.rst index 89694afdfa3fec..7797592e70aeeb 100644 --- a/Doc/whatsnew/3.13.rst +++ b/Doc/whatsnew/3.13.rst @@ -791,6 +791,15 @@ tkinter :class:`tkinter.ttk.Style`. (Contributed by Serhiy Storchaka in :gh:`68166`.) +* 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_* 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 --------- diff --git a/Lib/test/test_tkinter/test_images.py b/Lib/test/test_tkinter/test_images.py index ef1c99f57c6f47..4e93aac7b27514 100644 --- a/Lib/test/test_tkinter/test_images.py +++ b/Lib/test/test_tkinter/test_images.py @@ -302,7 +302,37 @@ def test_copy(self): image2 = image.copy() self.assertEqual(image2.width(), 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_=(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_=(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_=(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_=(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): image = self.create() @@ -316,6 +346,13 @@ def test_subsample(self): self.assertEqual(image2.height(), 8) self.assertEqual(image2.get(2, 3), image.get(4, 6)) + image2 = image.subsample(2, from_=(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): image = self.create() image2 = image.zoom(2, 3) @@ -330,6 +367,118 @@ def test_zoom(self): self.assertEqual(image2.get(8, 12), image.get(4, 6)) self.assertEqual(image2.get(9, 13), image.get(4, 6)) + image2 = image.zoom(2, from_=(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_=(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_=(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_=(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_=(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_=(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_=(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_=(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): image = self.create() image.put('{red green} {blue yellow}', to=(4, 6)) diff --git a/Lib/tkinter/__init__.py b/Lib/tkinter/__init__.py index fd7b48e3519990..dd39aa0d5c6a97 100644 --- a/Lib/tkinter/__init__.py +++ b/Lib/tkinter/__init__.py @@ -4263,33 +4263,109 @@ def cget(self, option): def __getitem__(self, key): return self.tk.call(self.name, 'cget', '-' + key) - # XXX copy -from, -to, ...? - def copy(self): - """Return a new PhotoImage with the same image as this widget.""" + def copy(self, *, from_=None, zoom=None, subsample=None): + """Return a new PhotoImage with the same image as this widget. + + The from_ 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 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) - self.tk.call(destImage, 'copy', self.name) + destImage.copy_replace(self, from_=from_, zoom=zoom, subsample=subsample) return destImage - def zoom(self, x, y=''): + def zoom(self, x, y='', *, from_=None): """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 direction. If y is not given, the default value is the same as x. + + The from_ option specifies a rectangular sub-region of the source + image to be copied, as in the copy() method. """ - destImage = PhotoImage(master=self.tk) if y=='': y=x - self.tk.call(destImage, 'copy', self.name, '-zoom',x,y) - return destImage + return self.copy(zoom=(x, y), from_=from_) - def subsample(self, x, y=''): + def subsample(self, x, y='', *, from_=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_ option specifies a rectangular sub-region of the source + image to be copied, as in the copy() method. """ - destImage = PhotoImage(master=self.tk) if y=='': y=x - self.tk.call(destImage, 'copy', self.name, '-subsample',x,y) - return destImage + return self.copy(subsample=(x, y), from_=from_) + + def copy_replace(self, sourceImage, *, from_=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_ 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 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_ is not None: + options.extend(('-from', *from_)) + 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): """Return the color (red, green, blue) of the pixel at X,Y.""" diff --git a/Misc/NEWS.d/next/Library/2024-04-24-16-07-26.gh-issue-118225.KdrcgL.rst b/Misc/NEWS.d/next/Library/2024-04-24-16-07-26.gh-issue-118225.KdrcgL.rst new file mode 100644 index 00000000000000..aa050352c7eda5 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2024-04-24-16-07-26.gh-issue-118225.KdrcgL.rst @@ -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_* parameter to :class:`!PhotoImage` methods +:meth:`!copy()`, :meth:`!zoom()` and :meth:`!subsample()`. Add *zoom* and +*subsample* parameters to :class:`!PhotoImage` method :meth:`!copy()`. From 0afa38129e5be5859cde66d0adb22f6d17916dff Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Mon, 6 May 2024 15:33:47 +0300 Subject: [PATCH 2/2] Rename "from_" to "from_coords". --- Doc/library/tkinter.rst | 2 +- Doc/whatsnew/3.13.rst | 2 +- Lib/test/test_tkinter/test_images.py | 26 +++--- Lib/tkinter/__init__.py | 79 ++++++++++--------- ...-04-24-16-07-26.gh-issue-118225.KdrcgL.rst | 2 +- 5 files changed, 57 insertions(+), 54 deletions(-) diff --git a/Doc/library/tkinter.rst b/Doc/library/tkinter.rst index f7d9c3682c6db4..7e5dee1b562df8 100644 --- a/Doc/library/tkinter.rst +++ b/Doc/library/tkinter.rst @@ -983,7 +983,7 @@ option (other options are available as well). 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_* parameter to :class:`!PhotoImage` methods :meth:`!copy()`, + 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()`. diff --git a/Doc/whatsnew/3.13.rst b/Doc/whatsnew/3.13.rst index 74a2f0fe1503ae..f601b88970a61e 100644 --- a/Doc/whatsnew/3.13.rst +++ b/Doc/whatsnew/3.13.rst @@ -878,7 +878,7 @@ tkinter * 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_* parameter to :class:`!PhotoImage` methods :meth:`!copy()`, + 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()`. diff --git a/Lib/test/test_tkinter/test_images.py b/Lib/test/test_tkinter/test_images.py index 4e93aac7b27514..2a59d014f4aa2a 100644 --- a/Lib/test/test_tkinter/test_images.py +++ b/Lib/test/test_tkinter/test_images.py @@ -304,14 +304,14 @@ def test_copy(self): self.assertEqual(image2.height(), 16) self.assertEqual(image2.get(4, 6), image.get(4, 6)) - image2 = image.copy(from_=(2, 3, 14, 11)) + 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_=(2, 3, 14, 11), zoom=2) + 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)) @@ -319,14 +319,14 @@ def test_copy(self): 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_=(2, 3, 14, 11), subsample=2) + 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_=(2, 3, 14, 11), subsample=2, zoom=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)) @@ -346,7 +346,7 @@ def test_subsample(self): self.assertEqual(image2.height(), 8) self.assertEqual(image2.get(2, 3), image.get(4, 6)) - image2 = image.subsample(2, from_=(2, 3, 14, 11)) + 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)) @@ -367,7 +367,7 @@ def test_zoom(self): self.assertEqual(image2.get(8, 12), image.get(4, 6)) self.assertEqual(image2.get(9, 13), image.get(4, 6)) - image2 = image.zoom(2, from_=(2, 3, 14, 11)) + 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)) @@ -384,7 +384,7 @@ def test_copy_replace(self): self.assertEqual(image2.get(4, 6), image.get(4, 6)) image2 = tkinter.PhotoImage(master=self.root) - image2.copy_replace(image, from_=(2, 3, 14, 11)) + 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)) @@ -393,7 +393,7 @@ def test_copy_replace(self): image2 = tkinter.PhotoImage(master=self.root) image2.copy_replace(image) - image2.copy_replace(image, from_=(2, 3, 14, 11), shrink=True) + 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)) @@ -401,7 +401,7 @@ def test_copy_replace(self): self.assertEqual(image2.get(2, 4), image.get(2+2, 4+3)) image2 = tkinter.PhotoImage(master=self.root) - image2.copy_replace(image, from_=(2, 3, 14, 11), to=(3, 6)) + 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)) @@ -409,7 +409,7 @@ def test_copy_replace(self): 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_=(2, 3, 14, 11), to=(0, 0, 100, 50)) + 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)) @@ -420,7 +420,7 @@ def test_copy_replace(self): 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_=(2, 3, 14, 11), zoom=2) + 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)) @@ -429,7 +429,7 @@ def test_copy_replace(self): 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_=(2, 3, 14, 11), subsample=2) + 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)) @@ -437,7 +437,7 @@ def test_copy_replace(self): 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_=(2, 3, 14, 11), subsample=2, zoom=3) + 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)) diff --git a/Lib/tkinter/__init__.py b/Lib/tkinter/__init__.py index bd03f202cefd34..35da83107bd99e 100644 --- a/Lib/tkinter/__init__.py +++ b/Lib/tkinter/__init__.py @@ -4279,65 +4279,68 @@ def cget(self, option): def __getitem__(self, key): return self.tk.call(self.name, 'cget', '-' + key) - def copy(self, *, from_=None, zoom=None, subsample=None): + def copy(self, *, from_coords=None, zoom=None, subsample=None): """Return a new PhotoImage with the same image as this widget. - The from_ 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 option is - not given, the default is the whole source image. + 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 + 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.copy_replace(self, from_=from_, zoom=zoom, subsample=subsample) + destImage.copy_replace(self, from_coords=from_coords, + zoom=zoom, subsample=subsample) return destImage - def zoom(self, x, y='', *, from_=None): + def zoom(self, x, y='', *, from_coords=None): """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 - direction. If y is not given, the default value is the same as x. + 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. - The from_ option specifies a rectangular sub-region of the source - image to be copied, as in the copy() method. + 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(zoom=(x, y), from_=from_) + return self.copy(zoom=(x, y), from_coords=from_coords) - def subsample(self, x, y='', *, from_=None): + 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. + but use only every Xth or Yth pixel. If Y is not given, the + default value is the same as X. - The from_ option specifies a rectangular sub-region of the source - image to be copied, as in the copy() method. + 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_=from_) + return self.copy(subsample=(x, y), from_coords=from_coords) - def copy_replace(self, sourceImage, *, from_=None, to=None, shrink=False, + 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_ 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 option is - not given, the default is the whole source image. + 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 + 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, @@ -4346,15 +4349,15 @@ def copy_replace(self, sourceImage, *, from_=None, to=None, shrink=False, 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 + 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 + 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 + 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 @@ -4364,8 +4367,8 @@ def copy_replace(self, sourceImage, *, from_=None, to=None, shrink=False, is used as-is. The default compositing rule is 'overlay'. """ options = [] - if from_ is not None: - options.extend(('-from', *from_)) + if from_coords is not None: + options.extend(('-from', *from_coords)) if to is not None: options.extend(('-to', *to)) if shrink: diff --git a/Misc/NEWS.d/next/Library/2024-04-24-16-07-26.gh-issue-118225.KdrcgL.rst b/Misc/NEWS.d/next/Library/2024-04-24-16-07-26.gh-issue-118225.KdrcgL.rst index aa050352c7eda5..a4671a301abb8a 100644 --- a/Misc/NEWS.d/next/Library/2024-04-24-16-07-26.gh-issue-118225.KdrcgL.rst +++ b/Misc/NEWS.d/next/Library/2024-04-24-16-07-26.gh-issue-118225.KdrcgL.rst @@ -1,5 +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_* parameter to :class:`!PhotoImage` methods +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()`.