https://github.com/python/cpython/commit/1b639a04cab0e858d90e2ac459fb34b73700701f
commit: 1b639a04cab0e858d90e2ac459fb34b73700701f
branch: main
author: Serhiy Storchaka <[email protected]>
committer: serhiy-storchaka <[email protected]>
date: 2024-05-06T17:33:15+03:00
summary:

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().

files:
A Misc/NEWS.d/next/Library/2024-04-24-16-07-26.gh-issue-118225.KdrcgL.rst
M Doc/library/tkinter.rst
M Doc/whatsnew/3.13.rst
M Lib/test/test_tkinter/test_images.py
M Lib/tkinter/__init__.py

diff --git a/Doc/library/tkinter.rst b/Doc/library/tkinter.rst
index e084d8554c7c09..7e5dee1b562df8 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_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
 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 0b75665ab9c9ba..67e7fbebc92ef4 100644
--- a/Doc/whatsnew/3.13.rst
+++ b/Doc/whatsnew/3.13.rst
@@ -882,6 +882,15 @@ tkinter
 * Add the :meth:`!after_info` method for Tkinter widgets.
   (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
 ---------
 
diff --git a/Lib/test/test_tkinter/test_images.py 
b/Lib/test/test_tkinter/test_images.py
index ef1c99f57c6f47..2a59d014f4aa2a 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_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):
         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_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):
         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_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):
         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 70a1ed46fd0774..35da83107bd99e 100644
--- a/Lib/tkinter/__init__.py
+++ b/Lib/tkinter/__init__.py
@@ -4278,33 +4278,112 @@ 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_coords=None, zoom=None, subsample=None):
+        """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)
-        self.tk.call(destImage, 'copy', self.name)
+        destImage.copy_replace(self, from_coords=from_coords,
+                               zoom=zoom, subsample=subsample)
         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
-        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_COORDS 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_coords=from_coords)
 
-    def subsample(self, x, y=''):
+    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_COORDS 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_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):
         """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..a4671a301abb8a
--- /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_coords* parameter to :class:`!PhotoImage` methods
+:meth:`!copy()`, :meth:`!zoom()` and :meth:`!subsample()`. Add *zoom* and
+*subsample* parameters to :class:`!PhotoImage` method :meth:`!copy()`.

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3/lists/python-checkins.python.org/
Member address: [email protected]

Reply via email to