By: CS2103-AY1819S1-T09-3
Since: Aug 2018
Licence: MIT
- 1. Setting up
- 2. Design
- 3. Implementation
- 4. Implementation: Canvas and Layer features
- 5. Implementation: Google features
- 6. Documentation
- 7. Testing
- 8. Dev Ops
- Appendix A: Product Scope
- Appendix B: User Stories
- Appendix C: Use Cases
- Appendix D: Non Functional Requirements
- Appendix E: Glossary
- Appendix F: Instructions for Manual Testing
- F.1. Launch and Shutdown
- F.2. Changing a directory
- F.3. Opening an image
- F.4. Traversing between image batches
- F.5. Saving data
- F.6. Applying transformation to image
- F.7. Applying raw transformation to image
- F.8. Creating new transformation
- F.9. Apply a customised transformation
- F.10. Save a image
- F.11. Canvases and Layers
-
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 JDK9
. -
IntelliJ IDE
ℹ️IntelliJ by default has Gradle and JavaFx plugins installed.
Do not disable them. If you have disabled them, go toFile
>Settings
>Plugins
to re-enable them.
-
Fork this repo, and clone the fork to your computer
-
Open IntelliJ (if you are not in the welcome screen, click
File
>Close Project
to close the existing project dialog first) -
Set up the correct JDK version for Gradle
-
Click
Configure
>Project Defaults
>Project Structure
-
Click
New…
and find the directory of the JDK
-
-
Click
Import Project
-
Locate the
build.gradle
file and select it. ClickOK
-
Click
Open as Project
-
Click
OK
to accept the default settings -
Open a console and run the command
gradlew processResources
(Mac/Linux:./gradlew processResources
). It should finish with theBUILD SUCCESSFUL
message.
This will generate all resources required by the application and tests. -
Open MainWindow.java and other files to check for any code errors
-
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
-
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
-
-
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)
-
Run the
seedu.address.MainApp
and try a few commands -
Run the tests to ensure they all pass.
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,
-
Go to
File
>Settings…
(Windows/Linux), orIntelliJ IDEA
>Preferences…
(macOS) -
Select
Editor
>Code Style
>Java
-
Click on the
Imports
tab to set the order-
For
Class count to use import with '*'
andNames count to use static import with '*'
: Set to999
to prevent IntelliJ from contracting the import statements -
For
Import Layout
: The order isimport static all other imports
,import java.*
,import javax.*
,import org.*
,import com.*
,import all other imports
. Add a<blank line>
between eachimport
-
Optionally, you can follow the UsingCheckstyle.adoc document to configure Intellij to check style-compliance as you write code.
When you are ready to start coding, get some sense of the overall design by reading Section 2.1, “Architecture”.
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.
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.
The sections below give more details of each 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 theModel
change. -
Responds to events raised from various parts of the App and updates the UI accordingly.
API :
Logic.java
-
Logic
uses thePiconsoParser
class to parse the user command. -
This results in a
Command
object which is executed by theLogicManager
. -
The command execution can affect the
Model
(e.g. converting an image) and/or raise events. -
The result of the command execution is encapsulated as a
CommandResult
object which is passed back to theUi
.
Given below is the Sequence Diagram for interactions within the Logic component for the execute("cd") API call.
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.
API : Storage.java
The Storage
component,
-
can save
UserPref
objects in json format and read it back.
This section describes some noteworthy details on how certain features are implemented.
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.
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:
ℹ️
|
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 .
|
This section contains the considerations and alternatives we had when implementing the cd command.
-
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.
-
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.
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.
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.
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.
The following sequence diagram shows how the open command works:
This section contains the considerations and alternatives we had when implementing the open command.
-
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.
-
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.
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 thecurrBatchPointer
by 10. -
UserPrefs#updateImageListPrevBatch()
- Minus thecurrBatchPointer
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.
Step 3. The user executes next
command to retrieve the next 10 images within Desktop.
Step 4. The user executes prev
command to retrieve the previous 10 images within Desktop.
The following sequence diagram shows how the next command works:
The following sequence diagram shows how the prev command works:
This section contains the considerations and alternatives we had when implementing the next/prev command.
-
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.
-
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 transformedBufferedImage
into thecache
folder (purge redundant images if needed). -
PreviewImage#getCurrentPath()
- Returns the current state’sPath
in thecache
folder for ImageMagick to use. -
PreviewImage#undo()
- Shifts thecurrentStatePointer
to the left, pointing to the previous state. -
PreviewImage#redo()
- Shifts thecurrentStatePointer
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.
|
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.
|
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.
|
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.
The following sequence diagram shows how the undo operation works:
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:
The following activity diagram summarizes what happens when a user executes the undo command:
The following activity diagram summarizes what happens when a user executes the redo command:
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 thePreviewImage’s
pointer to 0, pointing at the original state. -
redo-all
shifts thePreviewImage’s
pointer tocurrentSize - 1
, the state with all the applied transformations.
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:
-
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
andcolorspace
.
-
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.
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.
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.
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.
-
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.
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.
-
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.
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.
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.
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.
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 neithernull
or empty. -
getLayerNames()
- Returns a list of layer names in order. -
addLayer(PreviewImage, String)
- Adds a givenPreviewImage
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.
The above diagram illustrates the process of parsing and executing canvas
commands.
-
The user executes any command beginning with
canvas
. -
The command is first parsed by
PiconsoParser
which picks up thecanvas
keyword and passes any remaining arguments toCanvasCommandParser
. -
CanvasCommandParser
determines the appropriate sub-command being executed and performs the requested operation on the canvas.
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.
The layer
command follows a similar pattern as the canvas
command.
The following sub-commands inherit from LayerCommand
:
-
add
- Adds a layer from theCanvas
and generates a layer name. -
delete
- Removes a layer from theCanvas
. 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.
Layer
implements a few key accessors and utility functions. Some of them include:
-
addTranformation(Transformation)
- Adds a given transformation into itsPreviewImage
'sTransformationSet
. -
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.
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.
The above diagram illustrates the process of parsing and executing of canvas
commands.
-
The user executes any command beginning with
layer
. -
The command is first parsed by
PiconsoParser
which picks up thelayer
keyword and passes any remaining arguments toLayerCommandParser
. -
LayerCommandParser
determines the appropriate sub-command being executed and performs the requested operation on the canvas and current layer.
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.
As the canvas
and layer
commands compliment each other, let’s walk through a typical user’s session in Piconso from start to finish.
-
The user executes a valid
open
command: ACanvas
is constructed holding exactly oneLayer
with the default height and width being that of the image selected. -
The user adds a new layer to the canvas:
Canvas#addLayer
is executed and the helper functions inModelManager
refreshes the UI. -
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. -
While still on the original layer, the user moves it to the top left:
Layer#setPosition
moves the first layer out of the way. -
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. -
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. -
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 theapply resize
command. -
Realising his mistake, the user sets the canvas to fit his all of his layers:
Canvas#setCanvasAuto
toggles a boolean. -
The user fills in the default transparent background with a
canvas bgcolor #707070
: a hex color code is accepted byCanvas#setBackgroundColor
.
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 aPhotoHandler
instance, which handles matters related to Google commands. -
PhotoHandler
- Mainly consists of aPhotoLibraryClient
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 thePhotosLibraryClient
instance and parses results.-
The
PhotoHandler
instance is later accessed throughModel#getPhotoHandler()
andModel#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.
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. |
-
The user executes a
login
command. -
The login command calls
Model#getPhotoHandler()
and checks if a PhotoHandler instance already exists. If not, it calls static methodPhotoLibraryClientFactory#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.
-
User logs in through redirect window and a refresh token is returned and stored. PhotoLibraryClientFactory class then calls
PhotoLibraryClientFactory#createPhotosLibraryClient()
andPhotoLibraryClientFactory#getUserEmail()
to instantiate aPhotoHandler
instance -
The
PhotoHandler
instance is set by model asModel#photoLibrary
, and confirmation of login is sent to user.
The following sequence diagram illustrates how the above steps work:
-
Upon Piconso start up,
PhotoLibraryClientFactory#loginUserIfPossible()
is run byModelManager
-
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
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.
ℹ️
|
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.
|
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 updatesimageMap
andalbumMap
-
returnAllAlbumsList()
- CallsretrieveAllAlbumsFromGoogle()
and returns aList
of Album names for easy display -
retrieveAllAlbumsFromGoogle()
- Makes a Google API call and retrieves all albums stored in Google Photos, storing it inalbumMap
.-
Similar methods for other variants exist, but only methods used for the example below will be listed.
-
-
The user executes a
g ls /a
command. -
The command goes through parsing, firstly by
GoogleCommandParser
and secondly filtered by GoogleLsCommand. It is determined to be for listing albums, andmodel.getPhotoHandler().returnAllAlbumsList()
is called. -
Within that method, it makes a request to Google Photos, retrieves a
List<Album>
, and stores them in aMap<String, Album>
within the PhotoHandler instance, with each key being the album name, any duplicate names are renamed. -
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:
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.
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 upong 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
org 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.
-
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 theMediaItem
-
Similar methods for other variants exist, but only methods used for the example below will be listed.
-
ℹ️
|
For all upload/download related commands, the amount of time taken to process the task varies with the number of images to upload/download. |
-
The user executes a
g dl /i<b.png>
command. -
The command goes through parsing, firstly by
GoogleCommandParser
and secondly filtered by GoogleDlCommand. It is determined to be for downloading one image, andmodel.getPhotoHandler().downloadImage()
is called. -
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. -
Feedback of success is sent to the user
The process is similar for the other 2 variants, except images or images from a specific album are downloaded instead.
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.
-
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 aNewMediaItem
-
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.
-
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.
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.
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. |
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.
See UsingTravis.adoc to learn how to deploy GitHub Pages using Travis.
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.
-
Follow the instructions in UsingGradle.adoc to convert the AsciiDoc files in the
docs/
directory to HTML format. -
Go to your generated HTML files in the
build/docs
folder, right click on them and selectOpen with
→Google Chrome
. -
Within Chrome, click on the
Print
option in Chrome’s menu. -
Set the destination to
Save as PDF
, then clickSave
to save a copy of the file in PDF format. For best results, use the settings indicated in the screenshot below.
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.
|
Attribute name | Description | Default value |
---|---|---|
|
The name of the website. If set, the name will be displayed near the top of the page. |
not set |
|
URL to the site’s repository on GitHub. Setting this will add a "View on GitHub" link in the navigation bar. |
not set |
|
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 |
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.
|
Attribute name | Description | Default value |
---|---|---|
|
Site section that the document belongs to.
This will cause the associated item in the navigation bar to be highlighted.
One of: * Official SE-EDU projects only |
not set |
|
Set this attribute to remove the site navigation bar. |
not set |
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 |
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 chooseRun '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
)
We have two types of tests:
-
GUI Tests - These are tests involving the GUI. They include,
-
System Tests that test the entire App by simulating user actions on the GUI. These are in the
systemtests
package. -
Unit tests that test the individual components. These are in
seedu.address.ui
package.
-
-
Non-GUI Tests - These are tests not involving the GUI. They include,
-
Unit tests targeting the lowest level methods/classes.
e.g.seedu.address.commons.StringUtilTest
-
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
-
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
-
See UsingGradle.adoc to learn how to use Gradle for build automation.
We use Travis CI and AppVeyor to perform Continuous Integration on our projects. See UsingTravis.adoc and UsingAppVeyor.adoc for more details.
We use Coveralls to track the code coverage of our projects. See UsingCoveralls.adoc for more details.
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.
Here are the steps to create a new release.
-
Update the version number in
MainApp.java
. -
Generate a JAR file using Gradle.
-
Tag the repo with the version number. e.g.
v0.1
-
Create a new release using GitHub and upload the JAR file you created.
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)
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
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 |
(For all use cases below, the System is Piconso
and the Actor is the user
, unless specified otherwise)
MSS
-
User opens an image
-
User uses the CLI to describe a set of transformations to the image
-
Piconso shows a preview of the outcome
-
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.
-
MSS
-
User edits an image (from use case Editing an image)
-
Piconso displays transformations done on the left side pane
-
User enters command to save the set of transformations
-
Piconso requests for a name for the set
-
User enters a name
-
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.
-
MSS
-
User opens an image
-
Piconso displays the image in the preview pane
-
User adds caption or watermark image through input
-
Piconso displays the caption/watermark in the preview pane atop the first layer.
Use case ends.
MSS
-
User request to login to Google Photos
-
Piconso redirects the user to a login browser page
-
User logs in through browser page
-
Piconso shows feedback message for success
-
User uploads a photo
-
Piconso contacts Google Photos and uploads the photo
-
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.
-
-
Should work on any mainstream OS as long as it has Java 9 or higher installed.
-
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.
-
Should have easy to understand and remember user commands.
-
Allows users to navigate directories efficiently.
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. |
-
Initial launch
-
Download the jar file and copy into an empty folder
-
Double-click the jar file
Expected: Shows the GUI with a set of sample contacts. The window size may not be optimum.
-
-
Saving window preferences
-
Resize the window to an optimum size. Move the window to a different location. Close the window.
-
Re-launch the app by double-clicking the jar file.
Expected: The most recent window size and location is retained.
-
{ more test cases … }
-
Traversing between directories
-
Prerequisites: Users is aware of the current directory they are in from the status bar footer.
-
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. -
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. -
Test case (only for Windows):
cd C://Users
Expected: User is now inC:/Users
. The full path is shown in the status message. The current directory in the status bar is updated. -
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. -
Other incorrect cd commands to try:
cd C://xxxx
,cd xxxx
(where xxxx is a non-existent directory)
Expected: Similar to previous.
-
-
Tab
function to auto-complete directory name-
Prerequisites: Users is aware of the current directory they are in from the status bar footer.
-
Test case:
cd Desk
Expected:cd Desktop
is shown in the command box. -
Test case (only for Windows):
cd C://Use
Expected (only in Windows):cd C://Users
is shown in the command box. -
Test case:
cd 12345
Expected: User does not observe any changes. -
Other incorrect cd commands to try:
cd C://xxxx
,cd xxxx
(where xxxx is a non-existent directory)
Expected: Similar to previous.
-
-
Opening images in current directory
-
Prerequisites:
cd
into a existing directory with at at least 5 images to view the first batch of images in the directory. -
Test case:
open 1
Expected: The first image in theFilm Reel
is displayed on the GUI. The image opened is shown in the status message. -
Test case:
open 3
Expected: The third image in theFilm Reel
is displayed on the GUI. The image opened is shown in the status message. -
Test case:
open 0
Expected: The user is unable to open the image. Error message is shown in the status message. -
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.
-
-
Accessing the next/previous batch of images
-
Prerequisites:
cd
into a existing directory with 11-20 images to view the first batch of 10 images in the directory. -
Test case:
next
Expected: The next 10 images in the directory is shown in theFilm Reel
. The current batch and remaining number of images is shown in the status message. -
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. -
Test case:
prev
Expected: The first 10 images in the directory is shown in theFilm Reel
. The current batch and remaining number of images is shown in the status message. -
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. -
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.
-
-
Dealing with missing/corrupted data files
-
{explain how to simulate a missing/corrupted file and the expected behavior}
-
-
Apply build in transformation to image
-
Prerequisites:
cd
into a existing directory with at at least 1 image to view the first batch of images in the directory. -
Test case:
apply blur 0x8
Expected: the image in the preview panel is blurred, theblur 0x8
is shown in the history panel -
Test case:
apply rotate 180
Expected: the image in the preview panel is rotated 180 degrees, therotate 180
is shown in the history panel -
Test case:
apply blur
Expected: the image in the preview panel does not change, invalid message will be shown, command usage will be shown.
-
-
Apply raw transformation to image
-
Prerequisites:
cd
into a existing directory with at at least 1 image to view the first batch of images in the directory. -
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. -
Test case:
apply raw -blur 0x8
Expected: the image in the preview panel is blurred, the[-blur 0x8]
is shown in the history panel.
-
-
Create a new transformation
-
Test case:
create blurSample blur|0x8
Expected: message for new transformation created will be shown. -
Test case:
create blurSample blur
Expected: invalid argument message will be shown, command usage message will be shown.
-
-
Apply a customised transformation
-
Prerequisites: customised transformation
blurSample
has been created already. -
Test case:
apply @blurSample
Expected: the image in the preview panel is blurred, the@blurSample
is shown in the history panel. -
Test case:
apply blurSample
Expected: the image in the preview panel does not change, the invalid operation message will be shown.
-
-
save a edited image
-
Prerequisites: some transformations has been proceeded to image.
-
Test case:
save
Expected: The modified image will be save to the original image. -
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. -
Test case:
save test.test
Expected: A invalid format message will be shown, and the image will not be saved.
-
{ more test cases … }
-
Canvas manipulation
-
Positive cases
-
Test case:
canvas size 1000x1000
Expected: The canvas should now a size of 1000 by 1000 pixels. -
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. -
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 bysize
.
-
-
Negative cases
-
Test case:
canvas bgcolor invalid
-
Test case:
canvas size -123x456
-
Test case:
canvas auto-resize invalid
Expected: the help message demonstrating proper usage of the command should show up in the console output panel.
-
-
-
Layer manipulation
-
Positive cases
-
Test case:
layer add 1
Expected: The first image in theFilm Reel
is added as a new layer with on top of all existing layers. -
Test case:
layer remove 2
Expected: The second layer in the canvas should be removed if the following conditions are satisfied:-
The layer index provided is valid.
-
The layer is not currently selected.
-
The layer is not the last one in the canvas.
-
-
Test case:
layer select 2
Expected: The layer at index 2 is selected to be the current working layer, highlighted in theLayers Panel
and be the target of all subsequent transformations if the following condition is satisfied:-
The layer index provided is valid.
-
-
Test case:
layer swap 1 2
Expected: The layers at index 1 and 2 should be swapped if the following conditions are satisfied:-
The layer indexes provided are valid.
-
The layer indexes provided are distinct.
-
-
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 andauto-resize
isn’t turned on, clipping or complete invisibility should occur. Note that negative coordinates are accepted and this is intended behavior.
-
-
Negative cases
-
Test case:
layer add 11
. -
Test case:
layer remove 1
when there is only one layer left in the canvas. -
Test case:
layer remove 2
when index 2 is the layer that is selected. -
Test case:
layer swap -1 -3
. -
Test case:
layer swap 1 1
. -
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.
-
-