jeudi 24 juin 2021

Python Implementation of Multi-Dimensional Document Undo/Redo Pattern

I would consider my experience level in Python intermediate, and I am developing a logo creator/editor using Pillow. The objective of the program is to eventually create and save an Image. I would like to implement the ability to undo/redo editing actions. I am also interested in the ability to undo/redo individual layers globally.

I approached the design similar to Photoshop/Pixelmator, using multiple instances of Layer and one Logo. Through my research, I came across the Memento Design Pattern, and have attempted to adapt a similar design pattern (using a list as storage, which can hold a current state). According to my program, a Layer holds a PIL Image, and these Layers are held in a List within the Logo class. So when saving the Logo, the program starts with the background layer and progressively "stacks", or pastes the next Layer's Image on top of the last. I then My reasoning is that a Layer will allow me to keep editing history tied to the lifecycle of a Layer (i.e. when said layer is removed/deleted from the List, its "history" shall be removed from memory).

I have created a class DocumentRollback and an interface DocumentRollbackInterface. Both Layer and Logo utilize both of these classes to store history.

import abc

class DocumentRollback:

    def __init__(self, data):
        self.current = data
        self.index = 0
        self.history = [data]

    def save(self, data):
        self.current = data
        self.index += 1
        self.history.insert(self.index, data)

    def undo(self):
        if self.index > 0:
            self.current = self.history[self.index - 1]
            self.index -= 1

    def redo(self):
        if self.index + 1 < len(self.history):
            self.current = self.history[self.index + 1]
            self.index += 1

    def get_current(self):
        return self.current


class DocumentRollbackInterface:

    @abc.abstractmethod
    def undo(self):
        """
        Undo changes
        """

    @abc.abstractmethod
    def redo(self):
        """
        Redo changes
        """

    @abc.abstractmethod
    def destructure_to_dict(self):
        """
        Turn data to dict
        """

    @abc.abstractmethod
    def restructure_from_dict(self, data):
        """
        Turn dict to data
        """

When I create an instance of Layer or the Logo, I instantiate DocumentRollback (this will allow me to track the current state of said object), and inherit DocumentRollbackInterface (to make sure that methods are written to undo, redo, save, and restore data from the object's DocumentRollback).


class Logo(DocumentRollbackInterface, ABC):

    def __init__(self, background, name="Logo"):
        # background = Layer(name="background")
        """
        Create a new Logo. Must pass the background

        :param background:
        :type background Layer
        """
        background.rename_layer("background")
        self.layers = [background]
        self.image = background.canvas
        self.dimensions = background.dimensions
        self.name = name
        payload = {
            "layers": self.layers,
            "image": self.image,
            "dimensions": self.dimensions,
            "name": self.name
        }
        self.document_rollback = DocumentRollback(payload)

    def undo(self):
        self.document_rollback.undo()
        prev_state = self.document_rollback.get_current()
        self.restructure_from_dict(prev_state)

    def redo(self):
        self.document_rollback.redo()
        next_state = self.document_rollback.get_current()
        self.restructure_from_dict(next_state)

    def destructure_to_dict(self):
        return {
            "layers": self.layers,
            "image": self.image,
            "dimensions": self.dimensions,
            "name": self.name
        }

    def restructure_from_dict(self, data):
        self.layers = data['layers']
        self.image = data['image']
        self.dimensions = data['dimensions']
        self.name = data['name']

    # Continuation of Logo



class Layer(DocumentRollbackInterface, ABC):

    dimensions = (1024, 1024)
    transparent_color = Color(0, 0, 0, 0)

    def __init__(self, name, dimensions=dimensions, background_color=transparent_color):
        """
        Initializes a new Layer

        :param name: Name of Layer
        :type name str
        :param dimensions: Dimensions of new layer (width, height)
        :type dimensions tuple[int, int]
        :param background_color: Color of Background
        :type background_color Color
        """
        img = Image.new(mode="RGBA", size=dimensions, color=background_color.to_tuple())
        self.center = (int(dimensions[0] / 2), int(dimensions[1] / 2))
        self.canvas = img
        self.name = name
        self.dimensions = dimensions
        self.background_color = background_color
        payload = {
            "center": self.center,
            "canvas": self.canvas,
            "name": self.name,
            "dimensions": self.dimensions,
            "background_color": self.background_color
        }
        self.document_rollback = DocumentRollback(payload)

    def undo(self):
        self.document_rollback.undo()
        prev_state = self.document_rollback.get_current()
        self.restructure_from_dict(prev_state)

    def redo(self):
        self.document_rollback.redo()
        next_state = self.document_rollback.get_current()
        self.restructure_from_dict(next_state)
        
    def destructure_to_dict(self):
        return {
            "center": self.center,
            "canvas": self.canvas,
            "name": self.name,
            "dimensions": self.dimensions,
            "background_color": self.background_color
        }

    def restructure_from_dict(self, data):
        self.center = data['center']
        self.canvas = data['canvas']
        self.name = data['name']
        self.dimensions = data['dimensions']
        self.background_color = data['background_color']

    # Continuation of Layer

I have drawn a diagram of the main problem. enter image description here

Because I am using a list to store the history of state changes for each object, when the Logo is edited, the entire histories of all Layers stored in the DocumentRollback of the Logo are appended to the list. Presumably, the amount of memory required can grow exponentially (as indicated by the red highlighted Layers in the above diagram).

Ideally, I believe it would be wise to store a reference to the current state of each Layer inside the DocumentRollback of the Logo. I have a hunch that a LinkedList might work best because it allows for the direct forward and back pointers for undo and redo, but I am unsure if storing the current Node creates a reference to the Node, or an entirely new LinkedList gets constructed (similar to the case of the entire list that I currently have).

So the two main questions I have are:

  • What is the best way to reference the current state of the Layer and Logo that implements document history?
  • Is there a way to implement a "global index", or storage system that will allow different Layers to have actions undone/redone from the Logo? For example, in Photoshop, a user can start with a background image, then add another Layer and edit that, but is there a way to globally rollback the edits to each Layer from the "editor" (Logo in this case)?

Thank you so much! Any help is greatly appreciated!

Aucun commentaire:

Enregistrer un commentaire