Skip to content

Latest commit

 

History

History
1550 lines (1037 loc) · 75.4 KB

DeveloperGuide.adoc

File metadata and controls

1550 lines (1037 loc) · 75.4 KB

Piconso - Developer Guide

By: CS2103-AY1819S1-T09-3      Since: Aug 2018      Licence: MIT

1. Setting up

1.1. Prerequisites

  1. JDK 9 or later

    ⚠️
    JDK 10 on Windows will fail to run tests in headless mode due to a JavaFX bug. Windows developers are highly recommended to use JDK 9.
  2. IntelliJ IDE

    ℹ️
    IntelliJ by default has Gradle and JavaFx plugins installed.
    Do not disable them. If you have disabled them, go to File > Settings > Plugins to re-enable them.

1.2. Setting up the project in your computer

  1. Fork this repo, and clone the fork to your computer

  2. Open IntelliJ (if you are not in the welcome screen, click File > Close Project to close the existing project dialog first)

  3. Set up the correct JDK version for Gradle

    1. Click Configure > Project Defaults > Project Structure

    2. Click New…​ and find the directory of the JDK

  4. Click Import Project

  5. Locate the build.gradle file and select it. Click OK

  6. Click Open as Project

  7. Click OK to accept the default settings

  8. Open a console and run the command gradlew processResources (Mac/Linux: ./gradlew processResources). It should finish with the BUILD SUCCESSFUL message.
    This will generate all resources required by the application and tests.

  9. Open MainWindow.java and other files to check for any code errors

    1. Due to an ongoing issue with some of the newer versions of IntelliJ, code errors may be detected even if the project can be built and run successfully

    2. To resolve this, place your cursor over any of the code section highlighted in red. Press ALT+ENTER, and select Add '--add-modules=…​' to module compiler options for each error

  10. Repeat this for the test folder as well (e.g. check HelpWindowTest.java and other files for code errors, and if so, resolve it the same way)

1.3. Verifying the setup

  1. Run the seedu.address.MainApp and try a few commands

  2. Run the tests to ensure they all pass.

1.4. Configurations to do before writing code

1.4.1. Configuring the coding style

This project follows oss-generic coding standards. IntelliJ’s default style is mostly compliant with ours but it uses a different import order from ours. To rectify,

  1. Go to File > Settings…​ (Windows/Linux), or IntelliJ IDEA > Preferences…​ (macOS)

  2. Select Editor > Code Style > Java

  3. Click on the Imports tab to set the order

    • For Class count to use import with '*' and Names count to use static import with '*': Set to 999 to prevent IntelliJ from contracting the import statements

    • For Import Layout: The order is import static all other imports, import java.*, import javax.*, import org.*, import com.*, import all other imports. Add a <blank line> between each import

Optionally, you can follow the UsingCheckstyle.adoc document to configure Intellij to check style-compliance as you write code.

1.4.2. Getting started with coding

When you are ready to start coding, get some sense of the overall design by reading Section 2.1, “Architecture”.

2. Design

2.1. Architecture

Architecture
Figure 1. Architecture Diagram

The Architecture Diagram given above explains the high-level design of the App. Given below is a quick overview of each component.

💡
The .pptx files used to create diagrams in this document can be found in the diagrams folder. To update a diagram, modify the diagram in the pptx file, select the objects of the diagram, and choose Save as picture.

Main has only one class called MainApp. It is responsible for,

  • At app launch: Initializes the components in the correct sequence, and connects them up with each other.

  • At shut down: Shuts down the components and invokes cleanup method where necessary.

Commons represents a collection of classes used by multiple other components. Two of those classes play important roles at the architecture level.

  • EventsCenter : This class (written using Google’s Event Bus library) is used by components to communicate with other components using events (i.e. a form of Event Driven design)

  • LogsCenter : Used by many classes to write log messages to the App’s log file.

The rest of the App consists of four components.

  • UI: The UI of the App.

  • Logic: The command executor.

  • Model: Holds the data of the App in-memory.

  • Storage: Reads data from, and writes data to, the hard disk.

Each of the four components

  • Defines its API in an interface with the same name as the Component.

  • Exposes its functionality using a {Component Name}Manager class.

For example, the Logic component (see the class diagram given below) defines it’s API in the Logic.java interface and exposes its functionality using the LogicManager.java class.

LogicClassDiagram1
Figure 2. Class Diagram of the Logic Component

The sections below give more details of each component.

2.2. UI component

UiClassDiagram Piconso
Figure 3. Structure of the UI Component

API : Ui.java

The UI consists of a MainWindow that is made up of parts e.g.CommandBox, ResultDisplay, HistoryListPanel, FilmReel, ImagePanel etc. All these, including the MainWindow, inherit from the abstract UiPart class.

The UI component uses JavaFx UI framework. The layout of these UI parts are defined in matching .fxml files that are in the src/main/resources/view folder. For example, the layout of the MainWindow is specified in MainWindow.fxml

The UI component,

  • Executes user commands using the Logic component.

  • Binds itself to some images in the Model so that the UI can auto-update when data in the Model change.

  • Responds to events raised from various parts of the App and updates the UI accordingly.

2.3. Logic component

LogicClassDiagram1
Figure 4. Structure of the Logic Component

API : Logic.java

  1. Logic uses the PiconsoParser class to parse the user command.

  2. This results in a Command object which is executed by the LogicManager.

  3. The command execution can affect the Model (e.g. converting an image) and/or raise events.

  4. The result of the command execution is encapsulated as a CommandResult object which is passed back to the Ui.

Given below is the Sequence Diagram for interactions within the Logic component for the execute("cd") API call.

LogicComponentClassDiagram
Figure 5. Interactions inside the Logic Component for the cd command

2.4. Model component

ModelClassDiagram Piconso
Figure 6. Structure of the Model Component

API : Model.java

The Model,

  • stores a UserPref object that represents the user’s preferences.

  • stores the Canvas for the current image.

  • does not depend on any of the other three components.

2.5. Storage component

StorageClassDiagram
Figure 7. Structure of the Storage Component

API : Storage.java

The Storage component,

  • can save UserPref objects in json format and read it back.

2.6. Common classes

Classes used by multiple components are in the seedu.address.commons package.

3. Implementation

This section describes some noteworthy details on how certain features are implemented.

3.1. Change Directory (Cd) feature

The Cd feature is implemented to allow users to access images in the different directories within their home system. This removes the restrictions of accessing only images from one particular folder.

3.1.1. Current Implementation

The Cd mechanism is facilitated by the ModelManager. It contains the operations Model#getCurrDirectory() and Model#updateCurrDirectory(Path).

The Model calls upon UserPrefs within the respective operations. The UserPrefs class contains the current directory the user’s in, stored internally as currDirectory and implements the following operations:

  • UserPrefs#getCurrDirectory() - Returns the user’s current directory.

  • UserPrefs#updateUserPrefs(Path) - Updates the user’s current directory with the updated Path.

Within the operation UserPrefs#updateUserPrefs(Path), it also retrieves the list of images within the directory, which are stored internally as imageList. This would facilitate the open feature in our application (refer to 3.2).

Additionally, to ease user’s experience, similar to the actual usage of the cd command, this feature also uses the tab function to auto-complete the directory name if it exists.

ℹ️
Pressing tab again will display the next directory with the given prefix.

Given below is an example usage scenario and how the cd mechanism behaves at each step.

Step 1. The user launches the application for the first time. The UserPrefs will be initialized with the currDirectory as the user’s home directory.

Step 2. The user executes cd Desktop command to navigate into the Desktop directory. The cd command calls Model#getCurrDirectory() and appends Desktop to the end of the current directory. It then checks if the new Path is a directory and calls Model#updateCurrDirectory(Path) and updates the new Path in UserPrefs if the check returns true.

The following sequence diagram shows how the cd command works:

CdSequenceDiagram
Figure 8. Sequence Diagram for CdCommand
ℹ️
If the newCurrDirectory is not a directory, i.e. isDirectory() returns false, then there is no change in currDirectory state in UserPrefs. If so, it will return a failure message to the user rather than attempting to update currDirectory.

3.1.2. Design Considerations

This section contains the considerations and alternatives we had when implementing the cd command.

Aspect: How cd executes
  • Alternative 1 (current choice): Retrieves and updates current directory in UserPrefs.

    • Pros: Easy to implement and every command can access the current directory.

    • Cons: Appends and checks if path exists after every cd command entered.

  • Alternative 2 : Stores path that exists in a HashSet.

    • Pros: Do not need to append and check, and just check if it exists in HashSet.

    • Cons: Does not update existing path if user deletes a directory.

3.2. Open feature

The Open Command allow users to open the images in a batch of 10 images within the directory for image-editing. This command is further facilitated by the Next/Prev Command.

3.2.1. Current Implementation

The implementation of the Open feature is largely similar to the Cd Command. It is facilitated by the ModelManager and contains the following operations:

  • Model#getDirectoryImageList() - Retrieves the stored list of images in UserPrefs.

  • Model#updateCurrentOriginalImage(Image, Path) - Updates the model with the current image opened.

The Model calls upon UserPrefs to retrieve the imageList of the current batch. The UserPrefs class implements the following operation:

  • UserPrefs#getCurrImageListBatch() - Returns the current batch of images.

The Model#updateCurrentOriginalImage(Image, Path) operation stores the path of the opened image and the PreviewImage instance of it as currOriginalImage and currentPreviewImage within the ModelManager. Additionally, the operation also creates a canvas and a layer to facilitate the transformation feature. (refer to 3.5)

Given below is an example usage scenario and how the open mechanism behaves at each step.

Step 1. The user launches the application for the first time. The UserPrefs will be initialized with the currDirectory as the user’s home directory.

Step 2. The user executes cd Desktop command to navigate into the Desktop directory. The cd command will initialise the imageList with all the images within Desktop.

SelectCommand1

Step 3. The user executes open 1 command to open the first image in the first batch of 10 images. The open command calls Model#getDirectoryImageList() to retrieve the first batch of images within Desktop. The first image is then retrieved and displayed on the GUI.

OpenCommand2

Step 4. The user then executes open 5 command to open the fifth image in the batch of 10 images. The fifth image is then retrieved similarly and displayed on the GUI.

OpenCommand3

The following sequence diagram shows how the open command works:

OpenSequenceDiagram
Figure 9. Sequence Diagram for OpenCommand

3.2.2. Design Considerations

This section contains the considerations and alternatives we had when implementing the open command.

Aspect: How open executes
  • Alternative 1 (current choice): Open images within the BATCH_SIZE.

    • Pros: Users work on a small size of images.

    • Cons: Limited to the batch size.

  • Alternative 2 : Open images within the imageList size.

    • Pros: Easy to retrieve images anywhere in the list.

    • Cons: Can be very messy if the imageList size is too large.

3.3. Next/Prev feature

NextCommand retrieves the next 10 images in the image list (image list refers to images in the current directory).

On the contrary, the PrevCommand retrieves the previous 10 images in the image list.

3.3.1. Current Implementation

The implementation of the Next/Previous feature is similar to the CdCommand. It is facilitated by the ModelManager and contains the operations: Model#updateImageListNextBatch() and Model#updateImageListPrevBatch().

The Model calls upon UserPrefs which stores and facilitates the retrieval of the current batch of images using the currBatchPointer. The UserPrefs class implements the following operation:

  • UserPrefs#updateImageListNextBatch() - Adds the currBatchPointer by 10.

  • UserPrefs#updateImageListPrevBatch() - Minus the currBatchPointer by 10.

Given below is an example usage scenario and how the next/previous mechanism behaves at each step.

Step 1. The user launches the application for the first time. The UserPrefs will be initialized with the currDirectory as the user’s home directory.

Step 2. The user executes cd Desktop command to navigate into the Desktop directory. The cd command will initialise the imageList with all the images within Desktop.

SelectCommand1

Step 3. The user executes next command to retrieve the next 10 images within Desktop.

NextPrevCommand2

Step 4. The user executes prev command to retrieve the previous 10 images within Desktop.

NextPrevCommand3

The following sequence diagram shows how the next command works:

NextSequenceDiagram
Figure 10. Sequence Diagram for NextCommand

The following sequence diagram shows how the prev command works:

PrevSequenceDiagram
Figure 11. Sequence Diagram for PrevCommand

3.3.2. Design Considerations

This section contains the considerations and alternatives we had when implementing the next/prev command.

Aspect: How next/previous executes
  • Alternative 1 (current choice): Keeps track of current batch with a pointer.

    • Pros: Easy to access current batch images.

    • Cons: Efficiency might be lower for directories with many images.

  • Alternative 2 : Store image batches in array.

    • Pros: Fast to access next/previous batches.

    • Cons: Harder to handle changes in a batch (e.g. image got deleted) within the array.

3.4. Undo/Redo feature

3.4.1. Current Implementation

The undo/redo works on the currentLayer the user is working on. Each Layer contains a PreviewImage which facilitates the undo/redo mechanism. The mechanism works by caching the original image and transformed images in a temporary cache folder, and using currentStatePointer as a pointer together with currentSize as an indicator to manage the caching. Undoing and redoing will shift the currentStatePointer accordingly while each transformation commits the image by writing to the cache folder (purging redundant images if needed).

ℹ️
To work with multiple layers, each Layer has a single PreviewImage which is initialized with a unique LayerId, so that the PreviewImage can cache its images safely without conflicting image names.

Additionally, it implements the following operations:

  • PreviewImage#commit() - Writes the newly transformed BufferedImage into the cache folder (purge redundant images if needed).

  • PreviewImage#getCurrentPath() - Returns the current state’s Path in the cache folder for ImageMagick to use.

  • PreviewImage#undo() - Shifts the currentStatePointer to the left, pointing to the previous state.

  • PreviewImage#redo() - Shifts the currentStatePointer to the right, pointing to a previously undone state.

These operations are exposed in the Model interface as Model#updateCurrentPreviewImage(), Model#undoPreviewImage() and Model#redoPreviewImage().

Given below is an example usage scenario and how the undo/redo mechanism behaves at each step.

Step 1. The user opens an image with the open command. The Canvas is initialized with a new Layer which initializes its own PreviewImage with the opened image. The currentStatePointer is pointing to that state.

ℹ️
A Canvas can already be initialized, meaning this is an additional layer being added. The Layer and PreviewImage gets initialized the same way.
undoRedo1
Figure 12. After opening an image

Step 2. The user executes a series of transformations. Each time, the newly transformed BufferedImage is stored by writing it to the cache folder. The currentStatePointer is also incremented. Eg. hue, mirror, blur

ℹ️
If a command fails its execution, it will not call Model#updateCurrentPreviewImage(), so nothing will be cached.
undoRedo2
Figure 13. After executing transformations

Step 3. The user wants to undo the previous action by using the undo command. It will call Model#undoPreviewImage() which will shift the currentStatePointer once to the left, pointing it to the previous PreviewImage state. After which, that previously cached BufferedImage will be read and rendered to update the UI’s preview image pane.

ℹ️
If the currentStatePointer is at index 0, pointing to the initial state, then there are no previous states to restore. The undo command uses Model#canUndoPreviewImage() to check if this is the case. If so, it will return an error to the user rather than attempting to perform the undo.
undoRedo3
Figure 14. After exucuting undo

Step 4. The user executes another transformation, which calls Model#updateCurrentPreviewImage. Since the currentStatePointer is not pointing at the end state (currentSize - 1), and the states after the currentStatePointer will not make sense, all states after the pointer will be purged.

undoRedo4
Figure 15. After comitting and purging redundant states

The following sequence diagram shows how the undo operation works:

undoRedoSequenceDiagram
Figure 16. Sequence Diagram for Undo/RedoCommand

The redo command does the opposite — it calls Model#redoPreviewImage(), which shifts the currentStatePointer once to the right, pointing to the previously undone state.

ℹ️
If the currentStatePointer is at index currentSize - 1, pointing to the PreviewImage’s last state, then there are no undone states to restore. The redo command uses Model#canRedoPreviewImage() to check if this is the case. If so, it will return an error to the user rather than attempting to perform the redo.

The following activity diagram summarizes what happens when a user executes a new transformation:

undoRedoActivityDiagram
Figure 17. Activity Diagram for performing a new image transformation

The following activity diagram summarizes what happens when a user executes the undo command:

undoRedoActivityDiagram2
Figure 18. Activity Diagram for using undoCommand

The following activity diagram summarizes what happens when a user executes the redo command:

undoRedoActivityDiagram3
Figure 19. Activity Diagram for using redoCommand

3.4.2. Undo-all and Redo-all

The commands undo-all and redo-all follow the same implementation as undo and redo. They provide a convenient way to quickly undo and redo all transformations to the current layer’s PreviewImage for the user.

  • undo-all shifts the PreviewImage’s pointer to 0, pointing at the original state.

  • redo-all shifts the PreviewImage’s pointer to currentSize - 1, the state with all the applied transformations.

3.4.3. HistoryListPanel

The HistoryListPanel provides a view for the user to see the history of transformations applied. Whenever a transformation is done or when a undo/redo command is executed, the Model#refreshHistoryList() is executed which refreshes the HistoryListPanel with a HistoryUpdateEvent containing the current layer’s PreviewImage’s list of transformations. A view of the panel is shown below:

historyPanel
Figure 20. Image of HistoryListPanel

3.4.4. Design Considerations

Aspect: How undo & redo executes
  • Alternative 1 (current choice): Saves each newly transformed image (including original).

    • Pros: Fast execution of undo/redo as only reading the cached file is needed. Easy to implement.

    • Cons: Uses the user’s storage space temporarily for caching.

  • Alternative 2: Save only the command, and reverse/reapply transformation for each undo/redo.

    • Pros: Will not need to use user’s data storage.

    • Cons: Transformations on images take significantly more time as compared to reading and writing cache. Also, reversing of transformations are not possible for commands like blur and colorspace.

3.5. Transformations and ImageMagick

3.5.1. Current Implementation

The transformation mechanism is facilitated by ImageMagick, an external API Library that helps process the target image by the specified commands given. The transformation feature mainly consists of:

  • ImageMagicUtil: A class which will check, create, and run the imageMagick executable file

  • applyCommand: Applies the specified transformation to the image

  • createApplyCommand: Allows users to define their own custom transformation set.

The following describes main operations and processes contained in the classes above.

3.5.2. ImageMagickUtil:

This class is used to provide utility methods to facilitate interoperability with ImageMagick such as:

  • copyOutside() — Copies the OS-specific payload for ImageMagick.

  • parseOperationArguments() — Parse the build in arguments and customised operations, return a list of String passed into process builder.

  • processImage() — Converts user input to a valid ImageMagick command.

  • runProcessBuilder() — Given a valid ImageMagick command, runs the command and retrieves the output.

This class is separated from the model and acts as a Util for image editing, whenever editing the image, a transformation will be passed to the processImage, and it will be parses basing on the command templates stored in the app, then a list of arguments will be parsed from the transformation and a new process builder will be created and call the ImageMagick executable file to handle the image processing.

When starting the app, methods in ImageMagickUtil will be called to check the environment and prepare the ImageMagick package.

TransformationActivityDiagram
Figure 21. Activity Diagram for Transformation feature with ImageMagick
Design Considerations:

As ImageMagick is an independent portable package and it differs from system to system, in order to make sure that piconso will be able to work on multiple operating system, it needs to check the current system, choose the correct package and prepare the suitable environment for using ImageMagick.

3.5.3. ApplyCommand:

This command validates a transformation provided by the user and attempts to add it to the current PreviewImage's TransformationSet.

Given below is an example usage scenario and how the command should behave at each step.

Step 1. Upon first running the app, the corresponding zip package containing ImageMagick will be copied out of the jar and unzipped. A temporary folder for ImageMagick output is also created.

Step 2. The user opens an image with the open command. The method model.getPreviewImage() will be called to read the appropriate BufferedImage from disk.

Step 3. The user enters the command (e.g. apply blur 0x8). The entered command is parsed and the corresponding transformation is created.

ℹ️
If an invalid transformation is provided, a reminder of how to use the command will be given to the user and no ImageMagick command will be run.

Step 4. Call ImageMagick#processImage which is a quasi-factory method that creates an instance of ProcessBuilder which contains the all the necessary information for ImageMagick to apply a transformation to the image, executes the ProcessBuilder and reads the output back from the temporary folder.

ℹ️
While normal apply commands will show a useful help message after a failure, an apply raw command cannot provide the same granularity and will return a generic error message.

Step 5. If successful, the transformation that is just executed will be added to the PreviewImage's TransformationSet.

Step 6. The command will then request the UI to update its components, namely the HistoryListPanel and ImagePanel if any transformation was applied.

Design Considerations
  • As the ImageMagick executable file is only able to handle command line input and requires the path of the image to modify, intermediate images have to be stored in a temp folder first.

  • For senior users, or users who are familiar with ImageMagick, apply raw will provide more flexibility and enable them to do further image editting.

ApplyCommandSequenceDiagram
Figure 22. Sequence Diagram for Apply Command

3.5.4. CreateApplyCommand:

This command allows users to create a named set of commands and store in the disk. All transformation sets will first be validated by ImageMagickUtil#parseCustomiseCommand.

Given below is an example usage scenario and how the transformation should behave at each step.

Step 1. The user sequentially enters the transformations he wants to store, in the form of create [command name] (command|argument). For example, create blurgray blur|0x8 colorspace|GRAY will create a new command named 'blurgray'.

Step 2. The specified transformations are parsed and checked by the templates stored in the app.

ℹ️
If any of the specified transformations have an invalid name or argument, an error will be thrown.

Step 3. The validated new command will be stored in the json for in the PiconsoCommands folder create when first time run the app.

CreateTransformationActivityDiagram
Figure 23. Activity Diagram for CreateApply Command

3.5.5. Design Considerations

  • Combination of the transformations are commonly needed in our daily lives. When users design a serious of operations and plan to apply them to multiple images, or store them for future use, this feature will make the image editing more efficient and flexible, thus we implemented the create command.

3.6. Save feature

3.6.1. Current Implementation

The save command saves the current preview image to the same location it was opened in the local drive, User enters the name of the image and the format of the name will be validated.

  • isFormatValid() is to validate the format of the filename user enters

Given is an example of how to use the command

Step 1. Parse the argument entered, if no argument, store to original image.

Step 2. If there is an argument, parse the image name user enters, get the name and the format of the filename.

Step 3. Check the format of the image as well as the filename entered. If the format is not supported or the filename is already in use, a reminder will be shown to user and the image will not be saved.

3.6.2. Design Considerations

  • as there is already an option for user to save to original image, when user specifies a filename, in order to avoid unintended overwritten of other images, using existing filename is forbidden.

SaveCommandSequenceDiagram
Figure 24. Sequence Diagram for Save Command

4. Implementation: Canvas and Layer features

4.1. General overview

The Canvas and Layer classes serve as a layer of encapsulation for handling one or more instances of PreviewImage. This is in line with good defensive coding practices and separation of concerns.

4.2. Canvas (canvas) command

There are a set of overloaded command types inheriting from CanvasCommand, namely:

  • bgcolor - Sets the background colour of the canvas.

  • size - Sets the height and the width of the canvas.

  • auto-resize - Toggles the auto-resize property of the canvas.

4.2.1. Current implementation

Canvas manages various properties made accessible through the ModelManager class. Layer order is preserved as it important for image composition.

Canvas implements the following accessors and utility functions:

  • getLayers() - Returns an ordered list of layers which is guaranteed to be neither null or empty.

  • getLayerNames() - Returns a list of layer names in order.

  • addLayer(PreviewImage, String) - Adds a given PreviewImage into the canvas on its own layer.

  • getCurrentLayer() - Returns the current instance of the layer that is being operated on.

  • getCurrentLayerIndex() - Returns the index of the current instance of the layer that is being operated on.

  • setCurrentLayer(Index) - Sets the layer to be operated on.

  • removeLayer(Index) - Removes a layer at a given index. The current layer and the last remaining layer in a canvas cannot be removed.

All of these functions are exposed through the Model interface. Implementations in ModelManager usually include a call to the corresponding accessor along with utility functions to manage the state of the UI.

Beneath the hood, Canvas manipulation is powered by ImageMagick. All of the properties represented in Canvas are transformed into their respective flags or arguments in ImageMagick. For example, the isCanvasAuto property is transformed into the flag -flatten if false and -layers merge if true.

The processCanvas is a factory method that handles this transformation, returning an instance of a ProcessBuilder. The ProcessBuilder is then passed to ImageMagickUtils#runProcessBuilder which executes the process and stores the output in a BufferedImage.

CanvasSequenceDiagram
Figure 25. Sequence Diagram for Canvas Command

The above diagram illustrates the process of parsing and executing canvas commands.

  1. The user executes any command beginning with canvas.

  2. The command is first parsed by PiconsoParser which picks up the canvas keyword and passes any remaining arguments to CanvasCommandParser.

  3. CanvasCommandParser determines the appropriate sub-command being executed and performs the requested operation on the canvas.

4.2.2. Design considerations

Manipulation of the ProcessBuilder

Alternatives

Storing arguments directly in a List<T> where T is a custom type that implements Comparable<T> to ensure that arguments are in the correct order. A helper function will then map the T to a String and the resultant List<String> will be used to construct the necessary ProcessBuilder.

Evaluation

While insertion of new layers and properties will be extremely easy, modifying or removal of existing properties will involve searching through the entire list. As a result, this method is extremely hard to grok and performs poorly upon update or deletion of existing properties.

4.3. Layer (layer) commands

The layer command follows a similar pattern as the canvas command. The following sub-commands inherit from LayerCommand:

  • add - Adds a layer from the Canvas and generates a layer name.

  • delete - Removes a layer from the Canvas. The current layer and the last remaining layer cannot be removed.

  • select - Selects a layer to work on.

  • swap - Swaps the order of two distinct layers.

4.3.1. Current implementation

Layer implements a few key accessors and utility functions. Some of them include:

  • addTranformation(Transformation) - Adds a given transformation into its PreviewImage's TransformationSet.

  • getName() - Gets the name of the layer.

  • setHeight(int) - Sets the height of the layer.

  • setWidth(int) - Sets the width of the layer.

  • setPosition(int, int) - Sets the x and y coordinates of the layer.

The following image illustrates the coordinate system adopted in Piconso.

coords

The default anchor point is the top-left corner, this means that a layer position -10x-10 command will set that layer’s top left corner at (-10, -10). It is possible to have negative co-ordinates although clipping will occur unless the canvas is set to auto-resize.

LayerSequenceDiagram
Figure 26. Sequence Diagram for Layer Command

The above diagram illustrates the process of parsing and executing of canvas commands.

  1. The user executes any command beginning with layer.

  2. The command is first parsed by PiconsoParser which picks up the layer keyword and passes any remaining arguments to LayerCommandParser.

  3. LayerCommandParser determines the appropriate sub-command being executed and performs the requested operation on the canvas and current layer.

4.3.2. Design Considerations

Further manipulation of the ProcessBuilder

Alternatives

It is actually possible to nest ImageMagick commands which means that it is possible to keep separate List<T>s and conjugate them when the canvas needs to be rendered. The resultant ImageMagick command will take the form :

magick [overall canvas flags] {[canvas flag] (individual layer flags)} [overall canvas flags]

Where blocks enclosed by { } need to be repeated per layer.

Evaluation

This solution is the most straight-forward and results in no intermediate files which is usually desirable.

However, the resultant ImageMagick command will short circuit and cause the entire expression to fail if any of flags are incompatible or incorrect. Caching is also impossible, causing the whole canvas and all of its layers to be composed again from scratch. This results in a poor user experience and hence we have decided against it.

4.4. Canvas and Layers in action

As the canvas and layer commands compliment each other, let’s walk through a typical user’s session in Piconso from start to finish.

  1. The user executes a valid open command: A Canvas is constructed holding exactly one Layer with the default height and width being that of the image selected.

  2. The user adds a new layer to the canvas: Canvas#addLayer is executed and the helper functions in ModelManager refreshes the UI.

  3. The user swaps the order of the two layers to such that the original image is back on top: Canvas#swapLayer swaps the two entries in the list of layers.

  4. While still on the original layer, the user moves it to the top left: Layer#setPosition moves the first layer out of the way.

  5. The user decides to work on the second layer and enters layer select 2: Canvas#setCurrentLayer changes the current layer to the given index and internally keeps track of the index as well.

  6. A transformation is applied by the user to the current layer: Canvas#addTransformation appends the new Transformation to the current Layer. Note that no image processing occurs until a new render is requested.

  7. After a canvas size 80x60`command: The new `Canvas#setSize is applied upon the next render and it crops the image to a fraction of what it used to be. Remember that resizing and scaling is accomplished with the apply resize command.

  8. Realising his mistake, the user sets the canvas to fit his all of his layers: Canvas#setCanvasAuto toggles a boolean.

  9. The user fills in the default transparent background with a canvas bgcolor #707070 : a hex color code is accepted by Canvas#setBackgroundColor.

CanvasLayersDemo
Figure 27. Results of various commands in sequence. The black bounding rectangle indicates the canvas size.

5. Implementation: Google features

5.1. Overall Introduction

The Google commands allow for access to Google Photos through a logged-in instance of the user, and are held up by two main components.

  • PhotosLibraryClientFactory - Initiates and carries out the login process. Produces a PhotoHandler instance, which handles matters related to Google commands.

  • PhotoHandler - Mainly consists of a PhotoLibraryClient instance, the user’s logged in state, and 3 maps for storing images, albums, and specific images from an album. Performs all explicit calls to Google Photos through the PhotosLibraryClient instance and parses results.

    • The PhotoHandler instance is later accessed through Model#getPhotoHandler() and Model#setPhotoHandler()`.

There are 6 main google-related commands. The first two commands login/logout a user, and the latter 4 are mostly overloaded command types with GoogleCommand as the abstract parent class.

  • LoginCommand - Logs in user to their Google Account.

  • LogoutCommand - Logs a user out of their Google Account.

  • GoogleLsCommand - Lists files in Google Photos.

  • GoogleRefreshCommand - Refreshes the displayed list from Google Photos.

  • GoogleDlCommand - Downloads the specified image(s) from Google Photos to the user’s currently opened local directory.

  • GoogleUploadCommand - Uploads the specified image(s) from the user’s currently opened local directory to Google Photos.

We have connected and generated client_credentials.json via our own Google Account to enable usage of Google Photos API in Piconso. It is recommended that you configure Piconso to use Google Photos Library API with your own account rather than the provided.

If you are not familiar with how Google Photos works, it would be advisable to first try out Google Photos as a consumer before proceeding.

5.2. Login (login) Command

5.2.1. Current Implementation

The Login command currently authenticates a user via Google OAuth. To learn more about the implementation of OAuth methods, you may refer to O-Auth Explanation and Google API examples. As the workings of Google OAuth are rather complicated, it is suggested that you first go through the examples/documentations in those links.

The following static methods of PhotosLibraryClientFactory will be involved:

  • createClient() - Creates a PhotoLibrary instance, contains calls to Google OAuth related methods.

  • createPhotosLibraryClient(Credentials) - Creates an instance of Google’s PhotosLibraryClient using the provided credentials.

  • getUserEmail(Credential) - Retrieves the user’s email from Google+.

  • loginUserIfPossible() - Logs in a user if a valid credential file can be found during start up.

  • logoutUserIfPossible() - Logs a user out if a credential file can be found.

  • checkUserLogin() - Checks if any users are currently logged in.

Below are some examples on how the login command will work.

ℹ️
If connection to the internet is lost at any point during authentication with Google’s server, login will fail and an error message will be sent to user as feedback.

5.2.2. Scenario 1: Explicit login command executed, user not logged in yet.

  1. The user executes a login command.

  2. The login command calls Model#getPhotoHandler() and checks if a PhotoHandler instance already exists. If not, it calls static method PhotoLibraryClientFactory#createClient() to set up the requirements for log in and redirects the user to the browser.

This occurs asynchronously, and a user can close the redirect window and proceed as usual with the application if they have changed their mind.

  1. User logs in through redirect window and a refresh token is returned and stored. PhotoLibraryClientFactory class then calls PhotoLibraryClientFactory#createPhotosLibraryClient() and PhotoLibraryClientFactory#getUserEmail() to instantiate a PhotoHandler instance

  2. The PhotoHandler instance is set by model as Model#photoLibrary, and confirmation of login is sent to user.

The following sequence diagram illustrates how the above steps work:

LoginSequenceDiagram
Figure 28. Sequence Diagram for LoginCommand

5.2.3. Scenario 2: Implicit login, where Piconso auto logs in user upon re-launch

  1. Upon Piconso start up, PhotoLibraryClientFactory#loginUserIfPossible() is run by ModelManager

  2. The method checks for stored credentials (refresh token), and logs in user via PhotoLibraryClientFactory#createClient() if possible, else the log in process is skipped.

In both scenarios, whenever a valid refresh token is found stored the user is logged in without having to face browser re-direct again. At no point in time will we be storing a user’s actual credentials, but only a refresh token that allows us to keep a user logged in, actual credentials are handled by Google OAuth

5.2.4. Design Considerations

Asynchronous Login: By default, as long as a user has not logged in after login command is launched, Piconso freezes and waits for user input.

  • Current Solution: An asynchronous approach has been implemented instead such that Piconso continues working as usual even if the user has not logged in. Once a user is logged in, Piconso will simply show feedback.

  • Cons: Google allows max 3 login requests to be made in an interval, hence the login command will stop redirecting after 3 consecutive logins made in a short period of time. While it is unlikely this will pose a problem for users, it would be advisable to display the redirect URL in the Piconso CLI during future development.

5.2.5. Logout (logout) Command

The logout command deletes the stored credential file (and any other login-related files) if it exists, and does nothing if it does not. Upon deleting the file, the user will no longer have a refresh token to stay logged in, and thus is effectively logged out.

5.3. GoogleLsCommand (ls)

ℹ️
For all Google-related Commands (excluding login and logout), their command word is appended with a g (i.e g ls). Thus all children of GoogleCommand are first passed through a GoogleCommandParser to determine its type.

5.3.1. Current Implementation

The GoogleLsCommand allows users to browse through their stored images on Google Photos. Currently, it is overloaded with three types of commands the user can type

g ls → Lists all photos in user’s Google Photos, takes a longer amount of time depending on the number of images stored.
g ls /a → Lists all albums in user’s Google Photos.
g ls <ALBUM_NAME> → Lists all photos in specified album from Google Photos.

As such, parsing will be done twice. Once by GoogleCommandParser, and another within GoogleLsCommand itself.

The following methods and components of PhotoHandler will be involved:

  • imageMap - Map<String, MediaItem> that contains the list of images retrieved from Google API call. Uses image names as keys.

  • albumMap - Map<String, Album> that contains the list of albums retrieved from Google API call. Uses album names as keys.

  • specificAlbumMap - Map<String, MediaItem> that contains the list of images from a specific album retrieved from Google API call. Uses image names as keys.

  • getUniqueName(Map, String, String) - Checks for existence of a name in the map. If it exists, append a sutiable index at the end of the name.

  • refreshLists() - Re-retrieves all images and albums from Google Photos and updates imageMap and albumMap

  • returnAllAlbumsList() - Calls retrieveAllAlbumsFromGoogle() and returns a List of Album names for easy display

  • retrieveAllAlbumsFromGoogle() - Makes a Google API call and retrieves all albums stored in Google Photos, storing it in albumMap.

    • Similar methods for other variants exist, but only methods used for the example below will be listed.

5.3.2. Example: First call for g ls /a, where user wants to list all albums in Google Photos.

  1. The user executes a g ls /a command.

  2. The command goes through parsing, firstly by GoogleCommandParser and secondly filtered by GoogleLsCommand. It is determined to be for listing albums, and model.getPhotoHandler().returnAllAlbumsList() is called.

  3. Within that method, it makes a request to Google Photos, retrieves a List<Album>, and stores them in a Map<String, Album> within the PhotoHandler instance, with each key being the album name, any duplicate names are renamed.

  4. The entire list is converted into a String, and returned to the CommandBox UI as feedback to the user

The following activity diagram illustrates how the above steps work:

GoogleLsActivityDiagram
Figure 29. Activity Diagram for GoogleLsCommand

The process is similar for the other 2 variants, except images or images from a specific album are retrieved instead. All retrieved results are always stored in their respective maps.

5.3.3. Design Considerations

Performance Issues: The larger the amount of pictures stored in Google Photos, the longer amount of time a g ls command takes.

  • Alternative 1: Re-retrieve the list of photos/albums upon every ls call

    • Pro: Displayed list of images/albums is always up-to-date.

    • Con: Each Google Photos API call takes a notable amount of time to execute. Constantly re-retrieving the list of images/albums causes multiples API calls and incurs heavy waiting time for users.

  • Alternative 2 (Current): Adding a refresh command. As it is not likely that the set of photos in Google Photos will constantly change, the list of images/albums is retrieved and stored upon first g ls call and is only refreshed upon g refresh.

    • Pro: Shortens the waiting time for user on every g ls call

    • Con: More work on user’s behalf to remember to refresh.

Duplicated Naming: Google Photos allows for multiples images and albums to be stored with the same name, making it difficult to list item names as names might overlap.

  • Alternative 1 (Current): Upon a retrieval of images/albums from Google, image/album names are checked for duplicates. Every duplicate will have a suitable index appended at the end of its original name to ensures that unique naming occurs.

    • Con: If there are multiple images with the same name, the process of any g ls or g refresh will take a longer time to run.

  • Alternative 2: Instead of storing the name of the image/album as the key inside the Map, its unique ID (that is retrieved together with the files from Google) can be used instead.

    • Con: The ID too complicated for a user to input and remember.

Album Traversal: How to display and use Albums from Google

  • Alternative 1: Treat the album as a category, thus g ls <ALBUM_NAME> acts as a filter that filters photos by category.

    • Pro: Easy for users to understand and remember.

    • Con: The command to download from an album needs to be extended such that users need to specify which album the image to download is in i.e g dl /a<Album> /i<image.png>

  • Alternative 2: Handle the concept of albums like directories, such that a user can cd in and out of an album.

    • Con: Confusing for users to concurrently navigate and differentiate both google and local directories.

5.3.4. GoogleRefreshCommand (refresh)

The refresh command re-retrieves and stores the list of images/albums from Google Photos into respective maps in Model#getPhotoHandler().

5.4. GoogleDlCommand (dl)

5.4.1. Current Implementation

The GoogleDlCommand allows users to download their stored images on Google Photos. Currently, it is overloaded with three types of commands the user can type

g dl /i<IMAGE_NAME>: Downloads specified image from Google Photos
g dl /a<ALBUM_NAME>: Downloads all images from specified album in Google Photos, takes a longer amount of time depending on the number of images stored in the album.
g dl /a<ALBUM_NAME> /i<IMAGE_NAME>: Downloads a specific photo from a specific album in Google Photos.

As such, parsing will be done twice. Once by GoogleCommandParser, and another within GoogleDlCommand itself.

The following components of PhotoHandler will be involved:

  • downloadImage(String, String, String) - Ensures the request is valid and calls saveImageInDir()` if true.

  • saveImageInDir(MediaItem, String) - Saves the image using the baseURL of the MediaItem

    • Similar methods for other variants exist, but only methods used for the example below will be listed.

5.4.2. Example: g dl /i<b.png>`

ℹ️
For all upload/download related commands, the amount of time taken to process the task varies with the number of images to upload/download.
  1. The user executes a g dl /i<b.png> command.

  2. The command goes through parsing, firstly by GoogleCommandParser and secondly filtered by GoogleDlCommand. It is determined to be for downloading one image, and model.getPhotoHandler().downloadImage() is called.

  3. Within that method, it makes a request to Google Photos with the image’s respective ID, and successfully downloads the image into the user’s local directory.

    ⚠️
    If the local directory contains any image with the same name as the one to download, the image in the local directory will be replaced.
  4. Feedback of success is sent to the user

GoogleDlSequenceDiagram
Figure 30. Sequence Diagram for GoogleDlCommand

The process is similar for the other 2 variants, except images or images from a specific album are downloaded instead.

5.4.3. Design Considerations

Location of downloaded images:

  • Alternative 1 (Current): Download the image into user’s currently opened local directory.

    • Pro: User is able to easily find the new downloaded photo

    • Con: Images with the same name WILL be replaced and it will be messy when multiple photos are downloaded

  • Alternative 2 (Recommended for future): Images are downloaded into a specific directory which can be navigated to and fro via quick commands (i.e jump to PicDir, jump back PicDir)

    • Pro: Easier for user to retrieve and view downloaded photos. Photos with the same name can also be safely replaced as they are likely earlier versions downloaded from Google Photos.

5.5. GoogleUploadCommand (ul)

5.5.1. Current Implementation

The GoogleUploadCommand allows users to upload their images onto Google Photos. Currently, it is overloaded with two types of commands the user can type

g ul <IMAGE_NAME>: Uploads specified image from currently opened directory to Google Photos
g ul all: Uploads all images in currently open local directory.

As such, parsing will be done twice. Once by GoogleCommandParser, and another within GoogleUploadCommand itself.

As the implementation of GoogleUploadCommand is mostly dependent on understanding of the upload aspects of Google Photos API, an example will not be given. Please refer to this link.

The following components of PhotoHandler will be involved:

  • uploadImage(String, String) - Uploads the image from the current opened local directory.

  • retrievePiconsoAlbum() - Retrieves the ID of the Piconso Album in Google Photos. If it does not exist, command creates one.

  • uploadMediaItemsToGoogle(List<NewMediaItem>) - Highly Google Photos API based, returns a NewMediaItem

  • generateNewMediaImage(String, String) - Highly Google Photos API based, carries out actual upload to Google Photos with NewMediaItem.

    • Similar methods for other variants exist, but only methods used for the example below will be listed.

5.5.2. Design Considerations

Google Photos does not allow duplicate photos: Illustrated in the diagram below

  • No renaming of photos after upload allowed.

  • If an image is uploaded as "a.png", but exists with the exact same bytes as "b.png" on Google Photos, the image will replace the old image as b.png instead.

  • If two images with the same bytes are sent for upload, only the first will be uploaded.

uploadExplanation
Figure 31. Illustrates when how Google Photos handles images

Currently the only measure taken has been to state this to the user. As this issue was only discovered in the later stages of development, it does not have a workaround yet. Though a lot of refactoring will be required, switching from Google Photos to Google Drive might be a fix for the problem.

6. Documentation

We use asciidoc for writing documentation.

ℹ️
We chose asciidoc over Markdown because asciidoc, although a bit more complex than Markdown, provides more flexibility in formatting.

6.1. Editing Documentation

See UsingGradle.adoc to learn how to render .adoc files locally to preview the end result of your edits. Alternatively, you can download the AsciiDoc plugin for IntelliJ, which allows you to preview the changes you have made to your .adoc files in real-time.

6.2. Publishing Documentation

See UsingTravis.adoc to learn how to deploy GitHub Pages using Travis.

6.3. Converting Documentation to PDF format

We use Google Chrome for converting documentation to PDF format, as Chrome’s PDF engine preserves hyperlinks used in webpages.

Here are the steps to convert the project documentation files to PDF format.

  1. Follow the instructions in UsingGradle.adoc to convert the AsciiDoc files in the docs/ directory to HTML format.

  2. Go to your generated HTML files in the build/docs folder, right click on them and select Open withGoogle Chrome.

  3. Within Chrome, click on the Print option in Chrome’s menu.

  4. Set the destination to Save as PDF, then click Save to save a copy of the file in PDF format. For best results, use the settings indicated in the screenshot below.

chrome save as pdf
Figure 32. Saving documentation as PDF files in Chrome

6.4. Site-wide Documentation Settings

The build.gradle file specifies some project-specific asciidoc attributes which affects how all documentation files within this project are rendered.

💡
Attributes left unset in the build.gradle file will use their default value, if any.
Table 1. List of site-wide attributes
Attribute name Description Default value

site-name

The name of the website. If set, the name will be displayed near the top of the page.

not set

site-githuburl

URL to the site’s repository on GitHub. Setting this will add a "View on GitHub" link in the navigation bar.

not set

site-seedu

Define this attribute if the project is an official SE-EDU project. This will render the SE-EDU navigation bar at the top of the page, and add some SE-EDU-specific navigation items.

not set

6.5. Per-file Documentation Settings

Each .adoc file may also specify some file-specific asciidoc attributes which affects how the file is rendered.

Asciidoctor’s built-in attributes may be specified and used as well.

💡
Attributes left unset in .adoc files will use their default value, if any.
Table 2. List of per-file attributes, excluding Asciidoctor’s built-in attributes
Attribute name Description Default value

site-section

Site section that the document belongs to. This will cause the associated item in the navigation bar to be highlighted. One of: UserGuide, DeveloperGuide, LearningOutcomes*, AboutUs, ContactUs

* Official SE-EDU projects only

not set

no-site-header

Set this attribute to remove the site navigation bar.

not set

6.6. Site Template

The files in docs/stylesheets are the CSS stylesheets of the site. You can modify them to change some properties of the site’s design.

The files in docs/templates controls the rendering of .adoc files into HTML5. These template files are written in a mixture of Ruby and Slim.

⚠️

Modifying the template files in docs/templates requires some knowledge and experience with Ruby and Asciidoctor’s API. You should only modify them if you need greater control over the site’s layout than what stylesheets can provide. The SE-EDU team does not provide support for modified template files.

7. Testing

7.1. Running Tests

There are three ways to run tests.

💡
The most reliable way to run tests is the 3rd one. The first two methods might fail some GUI tests due to platform/resolution-specific idiosyncrasies.

Method 1: Using IntelliJ JUnit test runner

  • To run all tests, right-click on the src/test/java folder and choose Run 'All Tests'

  • To run a subset of tests, you can right-click on a test package, test class, or a test and choose Run 'ABC'

Method 2: Using Gradle

  • Open a console and run the command gradlew clean allTests (Mac/Linux: ./gradlew clean allTests)

ℹ️
See UsingGradle.adoc for more info on how to run tests using Gradle.

Method 3: Using Gradle (headless)

Thanks to the TestFX library we use, our GUI tests can be run in the headless mode. In the headless mode, GUI tests do not show up on the screen. That means the developer can do other things on the Computer while the tests are running.

To run tests in headless mode, open a console and run the command gradlew clean headless allTests (Mac/Linux: ./gradlew clean headless allTests)

7.2. Types of tests

We have two types of tests:

  1. GUI Tests - These are tests involving the GUI. They include,

    1. System Tests that test the entire App by simulating user actions on the GUI. These are in the systemtests package.

    2. Unit tests that test the individual components. These are in seedu.address.ui package.

  2. Non-GUI Tests - These are tests not involving the GUI. They include,

    1. Unit tests targeting the lowest level methods/classes.
      e.g. seedu.address.commons.StringUtilTest

    2. Integration tests that are checking the integration of multiple code units (those code units are assumed to be working).
      e.g. seedu.address.storage.StorageManagerTest

    3. Hybrids of unit and integration tests. These test are checking multiple code units as well as how the are connected together.
      e.g. seedu.address.logic.LogicManagerTest

7.3. Troubleshooting Testing

Problem: HelpWindowTest fails with a NullPointerException.

  • Reason: One of its dependencies, HelpWindow.html in src/main/resources/docs is missing.

  • Solution: Execute Gradle task processResources.

8. Dev Ops

8.1. Build Automation

See UsingGradle.adoc to learn how to use Gradle for build automation.

8.2. Continuous Integration

We use Travis CI and AppVeyor to perform Continuous Integration on our projects. See UsingTravis.adoc and UsingAppVeyor.adoc for more details.

8.3. Coverage Reporting

We use Coveralls to track the code coverage of our projects. See UsingCoveralls.adoc for more details.

8.4. Documentation Previews

When a pull request has changes to asciidoc files, you can use Netlify to see a preview of how the HTML version of those asciidoc files will look like when the pull request is merged. See UsingNetlify.adoc for more details.

8.5. Making a Release

Here are the steps to create a new release.

  1. Update the version number in MainApp.java.

  2. Generate a JAR file using Gradle.

  3. Tag the repo with the version number. e.g. v0.1

  4. Create a new release using GitHub and upload the JAR file you created.

8.6. Managing Dependencies

A project often depends on third-party libraries. For example, Address Book depends on the Jackson library for XML parsing. Managing these dependencies can be automated using Gradle. For example, Gradle can download the dependencies automatically, which is better than these alternatives.
a. Include those libraries in the repo (this bloats the repo size)
b. Require developers to download those libraries manually (this creates extra work for developers)

Appendix A: Product Scope

Target user profile:

  • needs a quick and easy way to edit images

  • has a lot of images to edit

  • appreciates the power that traditional editing software provides

  • familiar with the command line

  • prefers typing over mouse input

  • is reasonably comfortable using CLI app

Value proposition: view, preview and edit images quickly in a streamlined, modular and repeatable process

Appendix B: User Stories

Priorities: High (must have) - * * *, Medium (nice to have) - * *, Low (unlikely to have) - *

Priority As a …​ I want to …​ So that I can…​

* * *

photographer

edit an image

enhance an image

* * *

photographer

define my own set of transformations

avoid repetitive typing of commands

* * *

photographer

see the preview of the transformations before committing to disk

explore the effects of transformations

* *

photographer

be able to upload my photos to the cloud (Google Photos)

safely store and share my photos across devices

*

web developer or photographer

add captions or watermarks to images

to protect my intellectual property

*

web developer

quickly recolor icons

easily use them in re-skinning my application

Appendix C: Use Cases

(For all use cases below, the System is Piconso and the Actor is the user, unless specified otherwise)

Use case: Editing an image

MSS

  1. User opens an image

  2. User uses the CLI to describe a set of transformations to the image

  3. Piconso shows a preview of the outcome

  4. User can save the outcome/transformation to disk

    Use case ends.

Extensions

  • 1a. The given image cannot be opened.

    • 1a1. Piconso shows an error message.

      Use case resumes at step 1.

  • 2a. User is unsatisfied with the last transformation.

    • 2a1. User can undo the last transformation.

      Use case resumes at step 2.

Use case: Define a set of transformations

MSS

  1. User edits an image (from use case Editing an image)

  2. Piconso displays transformations done on the left side pane

  3. User enters command to save the set of transformations

  4. Piconso requests for a name for the set

  5. User enters a name

  6. Piconso saves the set

    Use case ends.

Extensions

  • 5a. The input name is already used.

    • 5a1. Piconso asks if user wants to overwrite to previously stored set.

      Use case resumes at step 4.

Use case: Add caption or watermark to image

MSS

  1. User opens an image

  2. Piconso displays the image in the preview pane

  3. User adds caption or watermark image through input

  4. Piconso displays the caption/watermark in the preview pane atop the first layer.

    Use case ends.

Use case: Upload to Google Photos

MSS

  1. User request to login to Google Photos

  2. Piconso redirects the user to a login browser page

  3. User logs in through browser page

  4. Piconso shows feedback message for success

  5. User uploads a photo

  6. Piconso contacts Google Photos and uploads the photo

  7. Piconso shows feedback for successful upload

    Use case ends.

Extensions

  • 3a. User does not login through browser window.

    • 3a1. User can resume on any use case not involving Google interaction.

  • 3b. Piconso loses internet connection.

    • 3b1. Piconso informs user of disconnection and suggests for them to try again.

      Use case resumes at step 1.

  • 5a. An error occurs while uploading.

    • 5a1. Piconso informs user of upload failure and suggests for them to try again.

      Use case resumes at step 5.

Appendix D: Non Functional Requirements

  1. Should work on any mainstream OS as long as it has Java 9 or higher installed.

  2. A user with above average typing speed for regular English text (i.e. not code, not system admin commands) should be able to accomplish most of the tasks faster using commands than using the mouse.

  3. Should have easy to understand and remember user commands.

  4. Allows users to navigate directories efficiently.

Appendix E: Glossary

Batch

Images that are currently being viewed. For example, if there are 16 images in the current directory, the default "batch" viewed would be the first 10 photos. Upon a next command, the "batch" switches to the next 10.

Transformation

Action that is performed on an image

Appendix F: Instructions for Manual Testing

Given below are instructions to test Piconso manually.

ℹ️
These instructions only provide a starting point for testers to work on; testers are expected to do more exploratory testing.

F.1. Launch and Shutdown

  1. Initial launch

    1. Download the jar file and copy into an empty folder

    2. Double-click the jar file
      Expected: Shows the GUI with a set of sample contacts. The window size may not be optimum.

  2. Saving window preferences

    1. Resize the window to an optimum size. Move the window to a different location. Close the window.

    2. Re-launch the app by double-clicking the jar file.
      Expected: The most recent window size and location is retained.

{ more test cases …​ }

F.2. Changing a directory

  1. Traversing between directories

    1. Prerequisites: Users is aware of the current directory they are in from the status bar footer.

    2. Test case: cd Desktop
      Expected: User is now in Desktop. The full path of Desktop is shown in the status message. The current directory in the status bar is updated.

    3. Test case: cd ..
      Expected: User is now in the previous directory. The full path is shown in the status message. The current directory in the status bar is updated.

    4. Test case (only for Windows): cd C://Users
      Expected: User is now in C:/Users. The full path is shown in the status message. The current directory in the status bar is updated.

    5. Test case: `cd `
      Expected: User is unable to traverse to any directory. Error message is shown in the status message. The status bar remains the same.

    6. Other incorrect cd commands to try: cd C://xxxx, cd xxxx (where xxxx is a non-existent directory)
      Expected: Similar to previous.

  2. Tab function to auto-complete directory name

    1. Prerequisites: Users is aware of the current directory they are in from the status bar footer.

    2. Test case: cd Desk
      Expected: cd Desktop is shown in the command box.

    3. Test case (only for Windows): cd C://Use
      Expected (only in Windows): cd C://Users is shown in the command box.

    4. Test case: cd 12345
      Expected: User does not observe any changes.

    5. Other incorrect cd commands to try: cd C://xxxx, cd xxxx (where xxxx is a non-existent directory)
      Expected: Similar to previous.

F.3. Opening an image

  1. Opening images in current directory

    1. Prerequisites: cd into a existing directory with at at least 5 images to view the first batch of images in the directory.

    2. Test case: open 1
      Expected: The first image in the Film Reel is displayed on the GUI. The image opened is shown in the status message.

    3. Test case: open 3
      Expected: The third image in the Film Reel is displayed on the GUI. The image opened is shown in the status message.

    4. Test case: open 0
      Expected: The user is unable to open the image. Error message is shown in the status message.

    5. Other incorrect open commands to try: open 11 (as index exceeds batch size of 10), open x (where x exceeds the total images in the current batch), open abc
      Expected: Similar to previous.

F.4. Traversing between image batches

  1. Accessing the next/previous batch of images

    1. Prerequisites: cd into a existing directory with 11-20 images to view the first batch of 10 images in the directory.

    2. Test case: next
      Expected: The next 10 images in the directory is shown in the Film Reel. The current batch and remaining number of images is shown in the status message.

    3. Test case: next
      Expected: The user is unable to access the next batch of images as it exceeds the total number of images in the list. Error message is shown in the status message.

    4. Test case: prev
      Expected: The first 10 images in the directory is shown in the Film Reel. The current batch and remaining number of images is shown in the status message.

    5. Test case: prev
      Expected: The user is unable to access the prev batch of images as it is already at the start of the list. Error message is shown in the status message.

    6. Other incorrect next commands to try: prev (when list the already at the start), next (when user is viewing the end of list)
      Expected: Similar to previous.

F.5. Saving data

  1. Dealing with missing/corrupted data files

    1. {explain how to simulate a missing/corrupted file and the expected behavior}

F.6. Applying transformation to image

  1. Apply build in transformation to image

    1. Prerequisites: cd into a existing directory with at at least 1 image to view the first batch of images in the directory.

    2. Test case: apply blur 0x8
      Expected: the image in the preview panel is blurred, the blur 0x8 is shown in the history panel

    3. Test case: apply rotate 180
      Expected: the image in the preview panel is rotated 180 degrees, the rotate 180 is shown in the history panel

    4. Test case: apply blur
      Expected: the image in the preview panel does not change, invalid message will be shown, command usage will be shown.

F.7. Applying raw transformation to image

  1. Apply raw transformation to image

    1. Prerequisites: cd into a existing directory with at at least 1 image to view the first batch of images in the directory.

    2. Test case: apply raw blur 0x8
      Expected: the image in the preview panel does not change, invalid operation message will be shown, nothing will be shown in the history panel.

    3. Test case: apply raw -blur 0x8
      Expected: the image in the preview panel is blurred, the [-blur 0x8] is shown in the history panel.

F.8. Creating new transformation

  1. Create a new transformation

    1. Test case: create blurSample blur|0x8
      Expected: message for new transformation created will be shown.

    2. Test case: create blurSample blur
      Expected: invalid argument message will be shown, command usage message will be shown.

F.9. Apply a customised transformation

  1. Apply a customised transformation

    1. Prerequisites: customised transformation blurSample has been created already.

    2. Test case: apply @blurSample
      Expected: the image in the preview panel is blurred, the @blurSample is shown in the history panel.

    3. Test case: apply blurSample
      Expected: the image in the preview panel does not change, the invalid operation message will be shown.

F.10. Save a image

  1. save a edited image

    1. Prerequisites: some transformations has been proceeded to image.

    2. Test case: save
      Expected: The modified image will be save to the original image.

    3. Test case: save test.jpg
      Expected: A new image test.jpg will be created in the current directory. The modified image will be saved to test.jpg.

    4. Test case: save test.test
      Expected: A invalid format message will be shown, and the image will not be saved.

{ more test cases …​ }

F.11. Canvases and Layers

  1. Canvas manipulation

    1. Positive cases

      1. Test case: canvas size 1000x1000
        Expected: The canvas should now a size of 1000 by 1000 pixels.

      2. Test case: canvas bgcolor none
        Expected: The canvas should now have a transparent background. If the canvas is too small, the background shouldn’t even be visible.

      3. Test case: canvas auto-resize on
        Expected: The canvas should now expand to ensure that all layers are within the frame. The canvas should ignore all options provided by size.

    2. Negative cases

      1. Test case: canvas bgcolor invalid

      2. Test case: canvas size -123x456

      3. Test case: canvas auto-resize invalid
        Expected: the help message demonstrating proper usage of the command should show up in the console output panel.

  2. Layer manipulation

    1. Positive cases

      1. Test case: layer add 1
        Expected: The first image in the Film Reel is added as a new layer with on top of all existing layers.

      2. Test case: layer remove 2
        Expected: The second layer in the canvas should be removed if the following conditions are satisfied:

        1. The layer index provided is valid.

        2. The layer is not currently selected.

        3. The layer is not the last one in the canvas.

      3. Test case: layer select 2
        Expected: The layer at index 2 is selected to be the current working layer, highlighted in the Layers Panel and be the target of all subsequent transformations if the following condition is satisfied:

        1. The layer index provided is valid.

      4. Test case: layer swap 1 2
        Expected: The layers at index 1 and 2 should be swapped if the following conditions are satisfied:

        1. The layer indexes provided are valid.

        2. The layer indexes provided are distinct.

      5. Test case: layer position -400x400
        Expected: The top-left corner of the layer will be set to the coordinate provided. If the coordinates are too far out of the canvas and auto-resize isn’t turned on, clipping or complete invisibility should occur. Note that negative coordinates are accepted and this is intended behavior.

    2. Negative cases

      1. Test case: layer add 11.

      2. Test case: layer remove 1 when there is only one layer left in the canvas.

      3. Test case: layer remove 2 when index 2 is the layer that is selected.

      4. Test case: layer swap -1 -3.

      5. Test case: layer swap 1 1.

      6. Test case: layer position invalid.
        Expected : A specific error message about the constraint violated should show up in the console output panel and no other operations should be performed.