Sample scripts

This section will show BeanShell/Groovy examples on how scripts can enhance your album in really cool ways. The examples here makes use of the huge library of ready-made java classes that Sun has provided for free and are directly accessible. To better understand the examples and to assist in the writing of your own scripts we strongly recommend that you first read through skin documentation, then bookmark the jAlbum API, Java API from Sun and Groovy documentation use them to look up classes and their methods. If you are new to Java it's recommended that you look at these tutorials first and that you then concentrate on the "java.lang", "java.util" and "java.io" packages. They are the ones that are most commonly used. Remenber that you can turn to the forum to get help and to help others!

Important note:If a skin calls getVars() during processing, that triggers the generation of all of the variables for the object, include things like the thumbnail dimensions. If the skin later does something that will alter those variables, as, for example, adding a filter to the engine, which might change those dimensions, the reported values will now be incorrect. The skin should, therefore, avoid using getVars() until all other actions that might change the object's variables have been done. If this can't be avoided, it is possible to cause the core to regenerate the variables for an object by executing obj.setVars(null).


If all of the code is not visible just select the whole of the code and copy it, then paste it into a text editor to see the missing bits


See Code Snippets for inspiration or help with your coding.


Contents

rootFolder & currentFolder

The variables rootFolder and currentFolder may not be accurate when used in an init script or external tool, as they come from a copy of the AlbumObject tree that jAlbum's explorer uses, and changes to the copy don't automatically propagate to the original AlbumObject tree. To get the data from the original AlbumObject tree you need to use

AlbumObject folder;
if(context.getExplorer() != null)
folder = context.getExplorer().getRootFolder(); // or currentFolder 
else
folder = rootFolder; // or currentFolder 

The test for context.getExplorer() being null or not is to ensure the code runs both in gui and console mode. The variables are guaranteed to be correct once init.bsh has been processed following a 'Make' operation.

res folder

If your skin requires files to be copied or added to the album’s res folder then ensure the folder exists, when making the album, by adding in init.groovy

resDirectory.mkdirs();

In fact, it is good practice to always check for the existence of a file/folder and, if required making it, before trying to access it.

Counting objects

You can write code to first collect counters for all categories in one count operation and then obtain the number of items for each category by using the count categories utility. The categories you can get counts for are audio, image, video, webPage, webLocation, folder and other

If the skin in use generates JSON files you can replace currentFolder or rootFolder with a webLocation that points to a jAlbum generated folder made with JSON files. If called on WebLocations pointing to other targets, null is returned.

<%
//Ways of getting the counters

//Counters for the current folder
CategoryCounters counters = JAlbumUtilities.countCategories(currentFolder, false);

//Counters for the root folder 
CategoryCounters counters = JAlbumUtilities.countCategories(rootFolder, false);

//Counters for the whole project, true uses recursion to count subfolder contents
CategoryCounters counters = JAlbumUtilities.countCategories(rootFolder, true);

//Examples for get the counts
Integer imageCount = counters.getCount(Category.image);
Integer webPageCount = counters.getCount(Category.webPage);

//A method call
//In init.groovy
CategoryCounters getCounts(AlbumObject folder, boolean recusive) {
	CategoryCounters counters = JAlbumUtilities.countCategories(folder, recusive);
	return counters;
}

//Then in index.htt or elsewhere
<% 
counters = getCounts(currentFolder, false);
Integer imageCount = counters.getCount(Category.image); 
out.println("imageCount = " + imageCount);
%>

Exclude from index pages folders that are empty or have only empty subfolders

This can prove useful if you start a project by making the folder structure first, and then in subsequent builds add files to those folders. Instead of having links to empty folders the album will only show thumbnails for folders with some content, even if it is in a further subfolder. Note, the empty folder and its contents are still made, it is just that they do not appear on index pages with content. If you really don't want empty folders in the album then see the code further down this section.

Example is based on a version of Minimal skin's index page thumbnail generation . Within the iterator we need to check if the current file is a folder or not. If it is then we need to see how many non-folder files are included in it and all of its subfolders. We set a boolean to true, so the default is to add thumbnails as normal, but we set that variable to false if a folder is empty or has only empty subfolders.

<%-- Iterate through images and produce an index table --%>
<div id="thumbnails">
  <ja:fileiterator>
  <%
    hasFiles = true; //Groovy global variable, by not declaring it a boolean
    if(currentFile.isDirectory()) {
      //Get the counters for this folder
      CategoryCounters counters = JAlbumUtilities.countCategories(currentObject, true);
      //Is the file count of this and all its subfolders 0? If yes, don't include it
      if(counters.getFileCount() == 0) hasFiles = false;
    }
  %>
  <ja:if test="${hasFiles}">
  <div>
    <a href="${closeupPath}">
      <ja:if exists="iconPath">
      <img class="noborder" src="${iconPath}" width="${thumbWidth}" height="${thumbHeight}" alt="${title}" title="${title}">
      </ja:if>
      <ja:else>
      <img src="${thumbPath}" width="${thumbWidth}" height="${thumbHeight}" alt="${title}" title="${title}">
      </ja:else>
      <%= getLabel(currentObject) %>
    </a>
  </div>
  </ja:if>
  </ja:fileiterator>
</div>

To exclude empty folders from the album, so they are not processed at all you can use this method. In init.groovy add

excludedFolders = new ArrayList();
AlbumSynchronizer AlSy = new AlbumSynchronizer(engine);
for(AlbumObject ao: rootFolder.getDescendants()) {
	if(ao.isFolder()) {
		counters = JAlbumUtilities.countCategories(ao, false);
			if(counters.getFileCount() == 0) {
			excludedFolders.add(ao);
			ao.setIncluded(false);
			AlSy.delete(ao);
		}
	}
}

To remove the excluded status after album build you can add in finally.groovy

if(excludedFolders.size() > 0) {
	for(AlbumObject po : rootFolder.getDescendants(IncludeOption.EXCLUDED)) {
		if(po.isFolder() && excludedFolders.contains(po)) {
			po.setIncluded(true);
		}
	}
}
window.albumExplorer.refreshAction.actionPerformed(null);

Extra slide pages for original images

When clicking on an image in a slide show, you may get to the original image, but it is not displayed in a html page of its own. The downside of this is that the surroundings for the image doesn't match the skin (usually displayed on white background) and you have to use the back navigation button to return. To address this issue duplicate your skin's 'slide.htt' file and rename it 'originalslide.htt'. Now, make the following simple adjustment to the "slide.htt" file and it will make sure that the "originalslide.htt" file gets processed if needed.

<%-- Image, maybe with link to original --%>
<ja:if exists="originalPath">
    <%-- Create a slide page for the original image too and link to that one  --%>
    <%-- instead of linking to an image --%>
    <%
       originalPage = originalPath;  //Default if no extra template page, String keyword not used to make originalPage a global variable
        File template = new File(skinDirectory, "originalslide.htt");
        if (template.exists()) {
            originalPage = label+"_orig"+engine.getPageExtension();
            engine.processTemplateFile(template,
            new File(outputDirectory,"slides/"+originalPage));
        }
    %>
    <a href="<%=originalPage%>">
        <img src="${imagePath}" />
    </a>
</ja:if>

Modify 'originalslide.htt' as required, such as the image src path so it points to the original instead of the slide image.

Generating extra slide images

If you want to offer additional sized images, such as for HTML’s srcset then this example code shows you how. It will generate extra images with image bounds of 400px, the extra images also have any filters applied as would the slide image.

In slide.htt:

<%
ai = new AlbumImage(currentObject);
ai = ai.scaleToFit(new Dimension(400,400));
ai = ai.applyFilters(JAFilter.CLOSEUPS_POSTSCALE_STAGE);
String name = label + "_medium.jpg";
File dest = new File(outputDirectory, engine.getSlideDirectory() + "/" + name);
ai.saveImage(dest);
mediumPath = IO.urlEncode(name); //keyword String not used, Groovy global variable
%>

Later in slide.htt, to display the new image:

<img src="${mediumPath}">

Note, the above method does not include any code to prevent the regeneration of the additional images with each 'make' of the album. That can be accomplished in various ways, for example using JAlbumUtilities.isDirty().

From jAlbum 24 you can simply use variants e.g.

<ja:picture>
  <img src="${imagePath}" width="${imageWidth}" height="${imageHeight}" alt="${title}">
</ja:picture>

That is expanded out, depending on the variants used, say image bounds 750x750 and variants of 1.33 and 0.66 to

<picture>
  <source type="image/webp" srcset="imageName-1000w.webp 1000w, imageName-500w.webp 500w"/>
  <source type="image/jpeg" srcset="imageName.JPG 750w"/>
  <img src="IMG_1099.JPG" width="750" height="600" alt="imageTitle">
</picture>

You can also use the ImageRender API to create your own variant sets. For example the code, in init.bsh, below will return an scrset of either jpeg or webp images

String getImageSet(AlbumObject ao, OutputFormat outf, String path)  {
	//String to hold the comma separated list of images
	String imageSet = "";
	//Check for the existence of an srcset
	if (ao.getVars().get("imageRenditions") != null) {
		//One exists, so get the renderer
		ImageRenderer renderer = ao.getRenderer();
		//Build an srcset of output format either WEBP or JPEG (or png, gif etc.)
		//Note the need for the ’slides/‘ path to get to the slides images
		imageSet = renderer.buildSrcset(renderer.getCloseupRenditions(outf), path);
		//To strip out the base image
		//imageSet = renderer.buildSrcset(ImageRenderer.stripTypical(renderer.getCloseupRenditions(outf)), path);
	}
	//Return the images of the required output format
	return imageSet;
}

Where you want your variants you would use

<%
//Call the above method for each image format
webpSet = getImageSet(currentObject, OutputFormat.WEBP, "slides/");
jpegSet = getImageSet(currentObject, OutputFormat.JPEG, "slides/");
%>

Then write the html code, such as with picture, source and sizes and a fallback jpeg src for browsers not supporting webp format.

<picture>
	<source
		sizes="(max-width: 770px) 600px,
		(max-width: 1000px) 750px,
		750px"																
		srcset="${webpSet}"
	type="image/webp">	
	<img
		sizes="(max-width: 770px) 600px,
		(max-width: 1000px) 750px,
		750px"																
		srcset="${jpegSet}"
	src="slides/imageName.jpg" alt="slides/imageName.jpg">
</picture>

The output of the above would look like

<picture>
	<source
		sizes="(max-width: 770px) 600px,
		(max-width: 1000px) 750px,
		750px"																
		srcset="slides/imageName-997w.webp 997w"
	type="image/webp">	
	<img
		sizes="(max-width: 770px) 600px,
		(max-width: 1000px) 750px,
		750px"																
		srcset="slides/imageName.jpg 750w,slides/imageName-495w.jpg 495w"
	src="slides/imageName.jpg" alt="slides/imageName.jpg">
</picture>

Getting metadata

File metadata can be accessed from the ‘meta’ object, use jAlbum’s list metadata to ensure both that the metadata exists and that the correct tag name is used. For example, to use an image’s IPTC Byline data use:

<%= meta.get("Iptc.Byline") %>

Multilevel (breadcrumb) parent links

You might have an album with many nested folders (animals/mamals/cats...). In this case it helps a lot to have all folder names displayed like this: animals » mamals » cats (being in the "cats" folder), with links to each parent folder. Copy and paste the scriptlet below into index.htt for this effect:

Note that the start tag has an exclamation mark after the percent symbol, this ensures the scripted method is only evaluated once making it faster and more memory efficient.

<%!
  void makeBreadcrumbs(AlbumObject folder, String prefix) {
    if (folder == null) {
      return;
    }
    makeBreadcrumbs(folder.getParent(), "../");
    String title = folder.getTitle();
    if ("".equals(title)) title = folder.getName();
    out.println("<a href=\"" + prefix + firstIndexPage + "\">" + title + "</a> » ");
  }

  void makeBreadcrumbs() {
    makeBreadcrumbs(currentFolder, "");
  }
%>

<% makeBreadcrumbs()%> <%-- sample call from index.htt --%>

<% makeBreadcrumbs(currentFolder, "../")%> <%-- sample call from slide.htt --%>

Reading captions/comments from separate text files

Some people wonder if it is possible to have jAlbum insert the contents of a text file having the same base name as an image but with ".txt" extension, for example "hiking.jpg" will get text from "hiking.txt". This is simple to do with the following scriptlet:

<!-- Extract text from textfiles carrying the same base name as this image -->
<ja:include page="<%= new File(imageDirectory, label+".txt") %>" />

Saving and getting object custom variables

Within an iterator of an index page (parsed before slides) you have access to the "current" variable, which is a Map object where you can store image specific variables and later retrieve them in the slides. Store like this:

current.put("variableName", variableData);

and get this data back for printing on a slide page like this

 <%= current.get("variableName") %>

Template save and load default settings

Skins can support templates such as About, Contact, Site maps etc. These templates can be included in every album a user may make, but entering the same data would be tedious. The code below can be easily added to any template to let users save and load default settings. There are two buttons, save and load, a single method called defaults and an action listener for each button. The code is shown in the context and structure of a typical template.

<ja:ui>
  import java.nio.file.*;
  import se.datadosen.util.IO;

   class AboutUI extends JWebPageUI {
    //Control Panel
    JButton saveAsDefault = new JButton("Save as default");
    JButton loadDefaults = new JButton("Load defaults");
    {
      add(new JLabel("Save/Load settings as defaults"));
      add("br", saveAsDefault);
      add("tab", loadDefaults);
    }
    init();
    void defaults(AlbumObject ao, boolean save) {
      // Skin properties
      SkinProperties skinProps = new SkinProperties(skinDirectory);
      String skinName = skinProps.getProperty(SkinProperties.TITLE) + " skin";

      //Setup the save and load setting functions not requiring currentAO
      File conFigDir = MiniConfig.getMiniConfig().configDir;
      //Create the defaults directories if needed for the save/load of the defaults.     
      String defaults = conFigDir.toString() + File.separator + "Template Defaults" + File.separator + skinName;
      Path defaultsDir= Paths.get(defaults);
      IO.createDirectories(defaultsDir);

      //Get associated files and make data Maps
      String templateName = currentAO.getName();
      //Get the settings default file
      File defaultsInfo = new File(conFigDir, "Template Defaults" + File.separator + skinName + File.separator + templateName + ".info");
      //Get the default comment property file
      defaultCommentFile = new File(conFigDir.toString() + File.separator + "Template Defaults" + File.separator + skinName + File.separator + templateName + "_comment.property");
      //The template's current settings file      
      File infoFile = new File(currentFolder.getFile(), ".jalbum" + File.separator + templateName + ".info");
      //Map the comment property
      if(save) {
        if(infoFile.exists()) {
          //Copy the current settings to the defaults
          IO.copyFile(infoFile, defaultsInfo);
          //Comments
          //Ensure there is a default file to write to
          if(!defaultCommentFile.exists()) IO.writeTextFile("About defaults\n", defaultCommentFile);
          //Map the comment property
          Map defaultsCommentMap = IO.readMapFile(defaultCommentFile);
          //Save the comment to the defaults map
          defaultsCommentMap.put("comment", currentAO.getComment());
          //Save the comment file to the default file  
          IO.writeMapFile(defaultsCommentMap, defaultCommentFile);
        }
        else {
          JOptionPane.showMessageDialog(context.getExplorer(), "<html>Template name has been changed,<br>settings not saved</html>", "Warning", JOptionPane.WARNING_MESSAGE);
        }      
      }
      else {
        //Load defaults
        if(infoFile.exists() && defaultsInfo.exists() && defaultCommentFile.exists()) {
          IO.copyFile(defaultsInfo, infoFile);
          //Comments  
          Map defaultsCommentMap = IO.readMapFile(defaultCommentFile);
          File commentsFile = new File(currentFolder.getFile(), ".jalbum" + File.separator + "comments.properties");
          Map commentsMap = IO.readMapFile(commentsFile);
          commentsMap.put(templateName, defaultsCommentMap.get("comment"));
          IO.writeMapFile(commentsMap, commentsFile);  
        }
        else {
          JOptionPane.showMessageDialog(context.getExplorer(), "<html>No defaults found for this template,<br>settings not loaded</html>", "Warning", JOptionPane.WARNING_MESSAGE);        
        }
      }
    } 

    //Monitor the saveAsDefault button
    saveAsDefault.addActionListener(new ActionListener() {
      public void actionPerformed(ActionEvent e) {
        //Save the current settings first
        saveUI();
        //Save settings as defaults
        defaults(currentAO, true);
        saveState();
        window.albumExplorer.refreshAction.actionPerformed(null);
      }
    });

    //Monitor the loadDefaults button
    loadDefaults.addActionListener(new ActionListener() {
      public void actionPerformed(ActionEvent e) {
        //Load default settings
        saveUI();
        defaults(currentAO, false);
          //Reload the UI, Reinitialise, save and refresh explore view
          loadUI();
          init();
          saveState();
          window.albumExplorer.refreshAction.actionPerformed(null);
      }
    });
  }
</ja:ui>

If a user tries to load default settings that don't exist, for a particular named template they are told they can't load settings. When saving all of the required directories and files are created if they don't already exist.

Theme images

Skins can easily create theme images for any folder, see explore/folder properties pane. By default theme image support is disabled, to enable it you need to provide a string variable called ‘folderImageSize’ with width and height dimension i.e. 1000x300 would be an image 1000px wide and 300px high.

Note, if you want to use an alternative variable name to folderImageSize you can do so by adding themeImageSizeName=yourtFolderImageSizeVariableName to the skin.properties file

If a skin supports WebP theme images they will be regenerated on every ‘Make’ operation.

The folderImageSize variable can be added to the SkinModel file as

public String folderImageSize = “1000x300”;

Or in onload.groovy as

public JTextField folderImageSize = new JTextField("1000x300");

Also by default the theme image will be based on the image selected for the folder thumbnail. If you want your skin to support separate thumbnail and theme images in the skin.properties file add

separateThemeImage=true

To produce the theme images there are three methods, let jAlbum make the theme images for you or take control and produce them yourself.

Note: Examples below show one possible method of adding and styling the theme image to the index.htt template, you can of course add them however you want.

Method 1: Built in routine Uses a skin property element that controls the editable status of the theme image size and cropping from a contextual menu. To allow access to this menu in the skin.properties file add

themeImageSizeName=themeImageSize
separateThemeImage=true  //or false
editableThemeImageSize=true

In init.groovy add

engine.setThemeImageProcessor(new ThemeImageProcessor());

In your skin's index.htt, where you want to include the theme image add, for example as a CSS background image for a class of 'themeImage'

<ja:if exists="themePath">
  .themeImage {
     background: url(${themePath}) center top no-repeat;
  }
</ja:if>

Method 2: jAlbum control: To init.groovy and

engine.setThemeImageProcessor(new ThemeImageProcessor());

In your skin's index.htt, where you want to include the theme image add, for example as a CSS background image for a class of 'themeImage'

<ja:if exists="themePath">
  .themeImage {
     background: url(${themePath}) center top no-repeat;
  }
</ja:if>

If you want other names or locations for the generated theme images you can create a subclass of the ThemeImageProcessor. In the example below the theme image is called ja_theme.jpg and it is put in the slides directory instead of the root of the current directory.

ThemeImageProcessor custom = new ThemeImageProcessor() {
  public String getThemePath(AlbumObject folder) {
    return engine.getSlideDirectory() + "/ja_theme.jpg";
  }
};
 
engine.setThemeImageProcessor(custom);

Note:There are three variables produced by jAlbum ${themePath}, ${themeWidth} and ${themeHeight}

Method 2A: jAlbum control: This method allows for the output format setting (Images>Advanced) to be honoured, jpg or webP. Here the theme image will be in the main folder, not under thumbs or slides folders.

To init.groovy and //Set the them image name

themeImageName = "folderimage.jpg";
 
if(engine.getOutputFormat().toString().equals("WebP")) {
	themeImageName = "folderimage.webp";
}
 
//Setup the custom theme image method
ThemeImageProcessor custom = new ThemeImageProcessor() {
  public String getThemePath(AlbumObject folder) {
    return themeImageName;
  }
};
 
//Invoke the theme image processor
engine.setThemeImageProcessor(custom);

Method 3: skin control: in postdir.groovy add

//Import required resources
import java.awt.Dimension;
import java.io.File;
import java.io.IOException;
import java.util.Map;
import java.util.logging.Level;
import net.jalbum.filters.CropFilter;
import se.datadosen.jalbum.AlbumImage;
import se.datadosen.jalbum.AlbumObject;
import se.datadosen.jalbum.EmbeddedProperties;
import se.datadosen.jalbum.JAlbum;
import se.datadosen.jalbum.JAlbumUtilities;

 //Get the theme image dimensions, jAlbum uses themeImageSizeName variable to get the dimensions
Dimension themeDim = engine.getThemeImageDim();
//Create an album object for the current folder’s theme image
AlbumObject theme = JAlbumUtilities.getThemeObject(currentFolder);
//Ensure dimension are set and an image object exists to work on
if (themeDim != null && theme != null) {
    File themeFile = new File(outputDirectory, "ja_theme.jpg");
    //Use builtin routines to compare the image to be used and the image generated, create/update the generated image only if required
    //for example crop focus changes or a different image selected
    EmbeddedProperties existing = new EmbeddedProperties(themeFile);
    EmbeddedProperties current = new EmbeddedProperties();
    current.put("themeSrc", theme.getPathFrom(currentFolder));
    current.put("themeDim", themeDim.width + "x" + themeDim.height);
    if (!engine.isAppendImages() || JAlbumUtilities.isDirty(themeFile, theme) || !existing.equals(current)) {
    //log information to the console, only visible when logging level is set to FINE in preferences/general
        JAlbum.logger.log(Level.FINE, "Regenerating theme image ja_theme.jpg (" + themeDim.width + "x" + themeDim.height + ") for folder {0} ", currentFolder);
        //Create the theme image using the dimension set and cropped as shown in the folder properties pane
        AlbumImage ai = new AlbumImage(theme);
        CropFilter cf = new CropFilter();
        double ratio = (double) themeDim.width / themeDim.height;
        int width = ai.getBufferedImage().getWidth();
        int height = ai.getBufferedImage().getHeight();
        Dimension bounds = new Dimension(width, (int) (width / ratio));
        if (bounds.height > height) {
                bounds = new Dimension((int) (height * ratio), height);
        }
        cf.setBounds(bounds);
        ai = ai.applyFilter(cf, (Map) theme.getVars());
        ai = ai.scaleToFit(themeDim);
        ai.saveImage(themeFile);
        current.write(themeFile);
    }
}

You can of course change the location and file name of the theme image by modifying the above code. For more information on the above code see: All that is left to do is include the theme image in your skin's pages, it is recommended you add the theme image as a 'background image'.

Skin properties API

JAlbumUtilities-getThemeObject()

EmbeddedProperties

setThemeImageDim()


Traversing the project

The standard way of traversing a nested structure such as a jAlbum project is to use recursion, which can seem non intuitive. Since jAlbum 15 an easier to grasp method has been available, the 'get descendants' API call. An example of using this is shown below, it generates a sorted unique list of keywords used throughout the project.

Set keywords = new TreeSet();
for (AlbumObject ao : rootFolder.getDescendants()) {
  keywords.addAll(ao.getKeywordSet());
}

Other examples are

// Count all but hidden and excluded objects
rootFolder.getDescendants().size();
 
// Count all but excluded objects
rootFolder.getDescendants(IncludeOption.HIDDEN).size();

Note, if not using Beanshell/Groovy/Java you will need to first use

var IncludeOption = Java.type("se.datadosen.jalbum.IncludeOption");

For more information an examples see this forum post by jAlbum's creator - of which the above is an extract of.