Building an image viewer. Part 2: Loading and saving images

This tutorial is a follow up of the building of the graphical user interface panel. In this tutorial we will load image files and show them in the image panel. Furthermore we will add the saving of images which will be needed in the next tutorial.

Goal of this tutorial

This tutorial shows you the basic concepts of loading and saving image files. You also learn about the file dialog and how it is used.

Some new attributes

The image viewer can either load single images or a folder containing images. If a folder is loaded, two buttons in the tool-bar can be used to switch through the contained images. In order to hold the necessary information for the loading of images we need to add some attributes to the image viewers class. This is done in the init method. In the next tutorial we want to apply changes to the image, so we will need to have access to it. The currently shown image is stored in currentImage. The paths of all images contained in a folder will be stored in imagesInFolder as a list. Initially there is no list, so we set the variable to void. Furthermore we need to have some index determining which is the current shown image in the list.

this.currentImage := void;

this.imagesInFolder := void;
this.imageFolderIndex := 0;

Since it is also possible to load a single image, we also need an attribute to store the path of the currently loaded image. This path is used for loading the image, as well as for saving it. We store the image’s path in the attribute shownImageFile. whenever there is a new value written to this attribute, a image has to be loaded. Here a Std.DataWrapper comes in handy.

this.shownImageFile := new Std.DataWrapper();

A callback function can be attached to it. This function is called, when the value of the wrapper is changed. In our case we will use the callback function to load the new image.

Loading the image file

As we need access to the image viewer object, we have to pass the this pointer to the function. The second argument of the function is loaded with the value which was set in the wrapper. To avoid null pointers we first check whether there was a value set to the wrapper. If not the function is left. To load the image we simply call gui.loadImage and pass the image’s path to it. The image itself is a GUI component, so if we want to display it, we only have to added it to the image panel. Before we do this lets first get rid of a potential old image. This is done by calling clear on the image panel. In general clear removes all children from a component. In a last step we update currentImage to the new image.

this.shownImageFile.onDataChanged += [this] => fn(imageViewer, file){
    if(!file)
        return;
        
    var image = gui.loadImage(file);
    
    imageViewer.imagePanel.clear();
    imageViewer.imagePanel.add(image);
    
    imageViewer.currentImage = image;
};

Some new methods

We also need two new method. One is needed to save an image to a file. The other traverses a folder and searches for all included image files. It adds the path to each found image to a list and returns that list.

Saving an image to disk

The method for saving an image is saveCurrentImageToFile, which saves the current image. The file path is passed to the method as parameter. Saving the image to disk works similar to loading an image. Again we first check the image and the file to be null pointers. As mentioned before, images are GUI components. To save the image we need to extract the actual bitmap from the component. We can access it via the image data of the component by calling currentImage.getImageData().getBitmap(). To save the image we can use Util.saveImage(bitmap, path).

ImageViewer.saveCurrentImageToFile := fn(file){
    if(!this.currentImage || !file)
        return;

    var bitmap = this.currentImage.getImageData().getBitmap();
    Util.saveBitmap(bitmap, file);
};

Getting all images of a folder

To switch through all images of a folder, we first need to extract the images from the folder. This is done by the method getAllImagesInFolder. If the extraction fails or there are no image files in the folder, the method returns void. As a first step it is checked whether the passed path is a folder. Otherwise it can not be processed and void is returned. To get all files of a folder we can use IO.dir(folder, FLAGS). As flag we pass IO.DIR_FILES, since we are only interested in files. The function returns a list, containing the files. Since we only want to process some image formats (PNG, JPEG and Bitmap) we need to filter the list. Therefore we iterate over the file list and check for each file whether its file ending is in the list of the supported formats. If this is the case, the file is added to the output list.

ImageViewer.getAllImagesInFolder := fn(folder){
    if(!folder || !IO.isDir(folder))
        return void;
    
    var images = [];
    var filter = [".png", ".jpg", ".bmp"];
    
    var files = IO.dir(folder, IO.DIR_FILES);
    
    foreach(files as var file){
        foreach(filter as var fileEnding){
            if(file.endsWith(fileEnding))
                images += file;
        }
    } 
    
    if(images.empty())
        return void;
    
    return images;
};

File dialog

In order to browse files PADrend offers a file dialog. We will use this dialog for both of our open functions as well as for the Save As function. The dialog can be set up for either opening a folder or opening a file. In the creation of the dialog a call back function is passed. This function is called if the dialog succeeds (e.g. the user presses the Confirm button).

Open a folder

Here we will finish the callback function of our Open Folder… menu item. First thing we need to do is to set up a file dialog. Therefore we instantiate a new GUI.FileDialog object. To the constructor we pass a title for the dialog, the start directory, a list of file endings that should be accepted and a callback function. We set the start directory to DIR, which is the current directory. Since we want to process folders only, we do not need any file endings, so that we pass void.
Next up we write our callback function. When the file dialog calls this function it passes the chosen folder to it. In the function we first set the index of the current image to 0, which is the first image in the list. Next up we call getAllImagesInFolder to collect the paths of all images included in the folder. If there is no image at all the function is left. Otherwise the first image is loaded.
After we have created the dialog we need to set it to open folders only. This is done by setting the folderSelector attribute of the instance to true. To open the dialog we simply call init() on the instance.

var diag = new GUI.FileDialog
(
    "Open Image Folder...",
    __DIR__,
    void,
    [imageViewer] => fn(imageViewer, folder){
        imageViewer.imageFolderIndex = 0;
        imageViewer.imagesInFolder = imageViewer.getAllImagesInFolder(folder);
        
        if(!imageViewer.imagesInFolder)
            return;
            
        imageViewer.shownImageFile(imageViewer.imagesInFolder[imageViewer.imageFolderIndex]);
    }
);
diag.folderSelector = true;
diag.init();

Open a file

The creation of a the file dialog is similar to the one for opening folders. However here we need to pass a list of file endings: [“.png”, “.jpg”, “.bmp”]. Also the callback function is adapted. Since we only want to show a single image, we set imagesInFolder to void. After that the image is loaded and shown.
When opening a file dialog, it will set some initial default file name. Since we want the text field, showing the currently chosen file, to be empty at the opening of the dialog, we set initialFilename to an empty string.

var diag = new GUI.FileDialog
(
    "Open Image File...",
    __DIR__,
    [".png", ".jpg", ".bmp"],
    [imageViewer] => fn(imageViewer, fileName){
        imageViewer.imageFolderIndex = 0;
        imageViewer.imagesInFolder = void;
        imageViewer.shownImageFile(fileName);
    } 
);
diag.initialFilename = "";
diag.init();

Save file and save file as

For Save File as… we again need a file dialog to figure out where to store the image. In the callback function we need to check whether the file already exists. If the file does not exist we can just save the image. If it exists we have to ask the user whether he wants to overwrite it. So we need to create some kind of message box. Therefore we use gui.createPopupWindow. We pass the windows size and the message to it. The window needs some actions, which are in our case a Yes and a No button. We add them by calling addAction. The Yes button gets a callback function which saves the image. Since the No button simply just closes the window, it does not need any callback function.

    if(!imageViewer.currentImage || !imageViewer.shownImageFile())
        return;

    var diag = new GUI.FileDialog
    (
        "Save File as...",
        __DIR__,
        [".png", ".jpg", ".bmp"],
        [imageViewer] => fn(imageViewer, fileName){
            if(IO.isFile(fileName)){
                var overWriteMsg = gui.createPopupWindow(300,100,"The file already exists. Do you want to overwrite it?");
                overWriteMsg.addAction(
                    "Yes", 
                    [imageViewer, fileName] => fn(imageViewer, fileName){
                        imageViewer.saveCurrentImageToFile(fileName);
                    });
                overWriteMsg.addAction("No");
                overWriteMsg.init();
            }
            else
                imageViewer.saveCurrentImageToFile(fileName);
        } 
    );
    diag.initialFilename = "";
    diag.init();

Save File… saves the current image to the file it was loaded from.

if(!imageViewer.currentImage || !imageViewer.shownImageFile())
    return;
    
imageViewer.saveCurrentImageToFile(imageViewer.shownImageFile());

Switching between the images of a folder

Next up we create the function for switching between the images of a folder. To switch to the next (previous) image, we increase (decrease) imageFolderIndex and load the corresponding image from imagesInFolder. To ensure the index to be valid, we first check whether that it is in bounds before accessing the list.

if(!imageViewer.imagesInFolder || imageViewer.imageFolderIndex == 0)
    return;

imageViewer.imageFolderIndex--;
imageViewer.shownImageFile(imageViewer.imagesInFolder[imageViewer.imageFolderIndex]);