Add additional options for the tkinter's PhotoImage copy method (original) (raw)

Right now, the PhotoImage copy method looks like this:

    # XXX copy -from, -to, ...?

    def copy(self):
        """Return a new PhotoImage with the same image as this widget."""
        destImage = PhotoImage(master=self.tk)
        self.tk.call(destImage, 'copy', self.name)
        return destImage

But with the all tk’s options, it will be possible to crop and scale images (and more).
Like this:

class MyPhotoImage(PhotoImage):
    def __init__(self, name=None, cnf={}, master=None, **kw):
        super().__init__(name, cnf, master, **kw)
    def copy(self, dest=None, from_coords=None, to=None, shrink=None,
             zoom=None, subsample=None, compositingrule=None):
        """Copies a region from the PhotoImage to the DEST
           (which must be a PhotoImage), possibly with pixel
           zooming and/or subsampling. If no options are specified, this
           command returns a new PhotoImage with the same image
           as this widget."""
        destImage = PhotoImage(master=self.tk) if not dest else dest
        args = (destImage, 'copy', self.name,)
        if from_coords:
            args = args + ('-from',) + tuple(from_coords)
        if to:
            args = args + ('-to',) + tuple(to)
        if shrink:
            args = args + ('-shrink',)
        if zoom:
            args = args + ('-zoom',) + tuple(zoom)
        if subsample:
            args = args + ('-subsample',) + tuple(subsample)
        if compositingrule:
            args = args + ('-compositingrule',) + tuple(compositingrule)
        self.tk.call(args)
        return destImage

tiles = MyPhotoImage(file="platformer_tiles.png")
tile = tiles.copy(zoom=(2, 2), from_coords=(0, 32, 32, 64))
cv.create_image(0, 0, image = tile, anchor=NW)

Tcl/tk man photo

tjreedy (Terry Jan Reedy) April 22, 2024, 1:02pm 2

Except for compositingrule added in 8.3, these options have been around since 8.0. I see no reason not to expose then in tkinter. @storchaka ?

However, as CPython generally returns None from pure mutation methods, I think there should be a new copyto method with the signature given. The harder part of a patch would be writing new tests (using an existing x.png already included in python.exe. @dvarkin, would you be willing to help with that? Can images reliably be compared? Are the shrink and zoom reliably deterministic across invocations and OSes?

dvarkin (Артём Шакиров) April 23, 2024, 12:27pm 3

Looking at the tests for PhotoImage, I found that PhotoImage already has zoom and subsample methods implemented with copy. Initially, I wanted to suggest adding only from_coords to copy, but I supplemented the example above with all the other options just in case. Nevertheless, I suggest leaving some of the options with copy for optimization (one tk copy call will be faster than several calls with zoom and subsample(?), and, on the contrary, separate zoom and copy methods will be more convenient in simple cases).
I also agree that it should be better with copyto.
Please check if I understood everything correctly.

class MyPhotoImage(PhotoImage):
    def __init__(self, name=None, cnf={}, master=None, **kw):
        super().__init__(name, cnf, master, **kw)
    def copy(self, from_coords=None, shrink=None, zoom=None, subsample=None):
        """Copies a region from the PhotoImage to a new PhotoImage,
           possibly with pixel zooming and/or subsampling. If no options
           are specified, this command returns a new PhotoImage
           with the same image as this widget."""
        destImage = PhotoImage(master=self.tk)
        args = (destImage, 'copy', self.name,)
        if from_coords:
            args = args + ('-from',) + tuple(from_coords)
        if shrink:
            args = args + ('-shrink',)
        if zoom:
            args = args + ('-zoom',) + tuple(zoom)
        if subsample:
            args = args + ('-subsample',) + tuple(subsample)
        self.tk.call(args)
        return destImage
    def copyto(self, dest, from_coords=None, to=None, shrink=None,
               zoom=None, subsample=None, compositingrule=None):
        """Copies a region from the PhotoImage to the DEST
           (which must be a PhotoImage), possibly with pixel
           zooming and/or subsampling."""
        args = (dest, 'copy', self.name,)
        if from_coords:
            args = args + ('-from',) + tuple(from_coords)
        if to:
            args = args + ('-to',) + tuple(to)
        if shrink:
            args = args + ('-shrink',)
        if zoom:
            args = args + ('-zoom',) + tuple(zoom)
        if subsample:
            args = args + ('-subsample',) + tuple(subsample)
        if compositingrule:
            args = args + ('-compositingrule',) + tuple(compositingrule)
        self.tk.call(args)
        

tiles = MyPhotoImage(file="platformer_tiles.png")
tile = tiles.copy(zoom=(2, 2), from_coords=(0, 32, 32, 64))
tiles.copyto(tile, zoom=(2, 2), from_coords=(0, 32, 32, 64), to=(64, 0))
cv.create_image(0, 0, image = tile, anchor=NW)

The copy method, along with dest, I suppose, does not need the to and compositingrule options, and if they are needed, they will remain in copyto.
I can also try to write tests, relying on tests for the zoom and subsample methods.

storchaka (Serhiy Storchaka) April 23, 2024, 8:19pm 4

I think it was already discussed on the bug tracker, but I cannot find the issue. Perhaps it was a side talk in an issue with not directly related title.

The main problem is that while Tkinter is mainly a thin wrapper around Tk, and methods of Tkinter classes are usually directly corresponding to Tk widget subcommands, it is not so for copy. The copy subcommand copies image from the specified source image to the “self” image. The PhotoImage.copy() method creates a copy of the “self” image. Completely different interfaces.

storchaka (Serhiy Storchaka) April 24, 2024, 7:36am 5

I am currently working on this. What would be the best name for the new method? copy_replace(), copy_inplace(), inplace_copy() or what? The interface should be:

destImage.copy_replace(sourceImage, from_=(60, 120, 0, 70), to=(20, 50))

I do not like to use replace() because it conflicts with other methods which usually return a copy of the original with changed fields. I do not like copy_from(), because it can be confused with the optional from_ parameter. I would like copy_into() or copy_to(), but reversing the direction of copying may conflict with future Tk extensions.

storchaka (Serhiy Storchaka) April 24, 2024, 1:17pm 6