Digital Lith Processing SDK

Contents

  1. Introduction
  2. Installation
  3. An Example Streaking Module
  4. An Example Process Module
  5. Disclaimerand Copyright
  6. Contact

Introduction

This is the documentation of the extension SDK for Digital Lith Processing

With version 3.0, Digital Lith Processing introduced another subset of uneven development - streaking. We started with something called Developer Drag which kind of produced a line like pattern on the image, depending on the surrounding image tones. Then with version 3.1 another streaking pattern has been added - Line Streaking. And the way the streaking is handled has also changed (under the hood).

Since there are so many possibilities to implement pattern generation and since not everyone might be interested in all of it, I decided to have the streaking feature to be modularized and extensible. So with version 3.1 there is the possibility to add streaking modules to digital lith and have them loaded dynamically. Or half dynamically as the extensions directory is only scanned for extensions on program start.

Then with version 4.0 another kind of modules is added: process modules. These let you define your own development process to replace the standard digital lith process. Although built-in by default (different from streaking modules, we can not go without a process module installed) the standard process uses the same sdk classes as described in this document.

The following will describe how you can develop, build and make available your very own streaking or process module.

Installation of the SDK

I assume that you have some experience in writing java code and using the java tools. Java SDK 1.8 is recommended.

In the meantime we have an updated verison of the sdk available. To install the SDK simply download digitallith_sdk4.5.3.jar and unpack it in the directory where you want to have it installed. The jar file contains the SDK jar file and the documentation.

You can find the api documentation for the sdk here: Click!

From version 3.4 to 3.5 there is a change in the streaking interface. Old modules are no longer supported.

Then from version 4.0 there is again a change. This time it is an extension to the old interface which allows parameters to be stored by names. Both version 3.5 and version 4.0 streaking modules were supported.

From version 4.1 on there old streaking modules are no longer supported. You will have to visit your code and adjust. It is not that much of a change, but since Digital Lith Processing is now running on double instead of floats we are no longer backwards compatible.

An Example Streaking Module

We now are going to implement a simple demo streaking module. The pattern we add will be a simple line grid placed over the image with a given strength. Simple enough to not get to complex, complex enough to show all uses.

We are going to use the new StreakingModuleV41 interface.

As in Line streaking we will provide a relative, an absolue and a paper only version. The parameters needed are number of horizontal and vertical lines as well as a strength parameter. With the absolute setting the number of lines will give the number of lines to be added, with the relative setting the number gives a percentage of lines which will used to build the pattern.

The Java Code

The SDK itself is pretty simple. There is an interface used in the streaking generation to report back progress to the UI. And then there is an interface that your streaking module has to implement. All else is packaging.

Here is the skeleton for the StreakingDemo implementation:

package de.zonev.demo;

import java.util.Random;
import java.util.List;
import java.util.ArrayList;
import java.awt.Dimension;
import java.awt.Point;
import de.zonev.digitalLith.sdk.StreakingModule;
import de.zonev.digitalLith.sdk.Progress;
import de.zonev.digitalLith.sdk.LogWriter;

public class StreakingDemo implements StreakingModule
{
   ...
}

Next we are going to define some constants which will come in useful later. First is the module id. The module id is a unique number to identify modules. If you want to exchange your module later, it would be good to request a module number. Please send a note and I will send you an id. If you are going to use the module just yourself, module ids above 90000 will never be used. Next is the streaking type identifiers these identifiers are also unique. When you have a module id then there is room for 100 identifiers.

  private static final int MODULE_ID = 90000;

  private static final int ABSOLUTE_TYPE_ID = MODULE_ID;
  private static final int RELATIVE_TYPE_ID = MODULE_ID+1;
  private static final int ABSOLUTE_PO_TYPE_ID = MODULE_ID+2;
  private static final int RELATIVE_PO_TYPE_ID = MODULE_ID+3;

The module needs to have a name and for user interface purposes we also need to specify the parameter names and the streaking type names as well as some defaults and the parameter encoding names.

  private static final String MODULE_NAME = "Demo Streaking";
  private static final String MODULE_VERSION = "1.0";
  
  private static final String [] PROVIDED_TYPE_NAMES = new String [] {
    "Demo (absolute)", "Demo (relative)", "Demo Paper Only (absolute)", "Demo Paper Only (relative)"
  };

  private static final int [] PROVIDED_TYPE_IDS = new int [] {
    ABSOLUTE_TYPE_ID, RELATIVE_TYPE_ID, ABSOLUTE_PO_TYPE_ID, RELATIVE_PO_TYPE_ID
  };

  private static final String TYPE_DESCRIPTION = "Demo Streaking provides a line streaking type.";

  private static final String [] PARAMETER_NAMES = new String [] {
    "Number Horizontal", "Number Vertical", "Strength"
  };

  private static final String [] PARAMETER_NAMES_WITH_ABSOLUTE_UNITS = new String [] {
    "Number Horizontal (positive number)", "Number Vertical (positive number)", "Strength (>=0, <=1)"
  };
  
  private static final String [] PARAMETER_NAMES_WITH_RELATIVE_UNITS = new String [] {
    "Number Horizontal (%)", "Number Vertical (%)", "Strength (>=0, <=1)"
  };

  private static final String [] PARAMETER_ENCODING_NAMES = new String [] {
    "horizontal", "vertical", "strength"
  };

  private static final String [] TOOL_TIPS = new String [] {
    "<html><p width=300>The number of horizontal lines to be added. This is either a percentage (in the relative case) " +
    "or an absolute number (in the absolute case).</p></html>",
    "<html><p width=300>The number of vertical lines to be added. This is either a percentage (in the relative case) " +
    "or an absolute number (in the absolute case).</p></html>",
    "<html><p width=300>The strength of the effect. This is a number between 0 and 1 and is comparable with the grain " +
    "parameter</p></html>"    
  };

  private static final double [] PARAMETER_DEFAULTS = new double [] {
    20f, 0f, 0.1f      
  };

Parameters are passed in as an array of doubles. So to make code more readable here are the indexes for the specific parameters.

  private static final int NUM_H = 0;
  private static final int NUM_V = 1;
  private static final int STRENGTH = 2;

Now we are ready to start with the implementation of the StreakingModuleInterface. The first few methods are pretty straight forward as we just need to return the constants above.

We start with the module id, the module name and the module version.

  @Override
  public int getModuleId()
  {
    return MODULE_ID;
  }

  @Override
  public String getModuleName()
  {
    return MODULE_NAME;
  }

  @Override
  public String getModuleVersionString()
  {
    return MODULE_VERSION;
  }

Next we provide the streaking type identifiers and their corresponding names and descriptions.

  @Override
  public String[] getProvidedTypeNames()
  {
    return PROVIDED_TYPE_NAMES;
  }

  @Override
  public int[] getProvidedTypeIds()
  {
    return PROVIDED_TYPE_IDS;
  }
  
  @Override
  public String getDescriptions (int streakingType)
  {
    return TYPE_DESCRIPTION;
  }

The parameter names will be shown in the input area for the streaking pattern. We need to hand them to the UI. And there should be a set of default values too.

  @Override
  public String[] getParameterNames()
  {
    return PARAMETER_NAMES;
  }

  @Override
  public String[] getToolTips()
  {
    return TOOL_TIPS;
  }

  @Override
  public double[] getDefaultParameterValues(int streakingType)
  {
    return copy(PARAMETER_DEFAULTS);
  }

And to store the parameters in presets we need to give the encoding names used in the preset.

  @Override
  public String[] getParameterEncodingNames()
  {
    return PARAMETER_ENCODING_NAMES;
  }

There is one more thing about parameter names. If the user specifies that the parameter names should be shown with units, then we need to hand back those parameter names. If you do not like to show units in your streaking module, just return the parameter names in this function. Since there might be absolute and relative parameters which differ in units (as in our case) the parameter names with units are requested per streaking type.

  @Override
  public String[] getParameterNamesWithUnits(int streakingType)
  {
    if (streakingType == ABSOLUTE_TYPE_ID || streakingType == ABSOLUTE_PO_TYPE_ID)
      return PARAMETER_NAMES_WITH_ABSOLUTE_UNITS;
    else
      return PARAMETER_NAMES_WITH_RELATIVE_UNITS;
  }

There are two approaches to invalid parameter values. You can warn the user about invalid values and stop the processing. Or you can adjust the invalid values to some useful ones. All the streaking modules coming with digital lith use the first approach. Here is how you go with that.

  @Override
  public List validateParameters (int streakingType, double [] params)
  {
    List invalidParameters = new ArrayList();
    if (params[STRENGTH] < 0)
      invalidParameters.add(PARAMETER_NAMES[STRENGTH]);
    if (params[NUM_H] < 0)
      invalidParameters.add(PARAMETER_NAMES[NUM_H]);
    if (params[NUM_V] < 0)
      invalidParameters.add(PARAMETER_NAMES[NUM_V]);
    return invalidParameters;
  }

In case you do not want to stop deeloping the image and rather go the approach in which you adjust the parameters, just return an empty list in the above function.

Next there is the function which is called to validate a set of parameters. In our case if the strength is zero or both of horizontal and vertical number of lines, then the set is not valid and we return an all zero'd out set of parameters.

  @Override
  public double[] validateParameterValues(double[] values)
  {
    double [] result = copy(values);
    if (result[NUM_H] == 0f && result[NUM_V] == 0f)
      result[STRENGTH] = 0f;
    else if (result[STRENGTH] == 0f) {
      result[NUM_H] = 0f;
      result[NUM_V] = 0f;
    }
    return result;
  }

In this case we would have also been good with just returning the values since there is the handling of invalid values with the validateParameters.

Keep in mind that in a future version we might deprecate this function and go with validating the parameters and stopping development only.

For streaking types that have a vertical or horizontal flavor the UI has a button to flip horizontal and vertical parameters. Our streaking module has such a flavor, so we implement it like the following

  @Override
  public double[] flipHorizontalAndVertical(double[] values)
  {
    double [] result = new double[3];
    result[STRENGTH] = values[STRENGTH];
    result[NUM_H] = values[NUM_V];
    result[NUM_V] = values[NUM_H];
    return result;
  }

The next method returns true if the given parameter values are valid.

  @Override
  public boolean isUsableParameter(double[] values)
  {
    double [] v = validateParameterValues(values);
    return v[STRENGTH] != 0f;
  }

Digital Lith UI needs to know if the horizontal/vertical flip button should be enabled or not. This is the method which provides the info:

  public boolean isHorizontalVertical ()
  {
    return true;
  }

You are able to provide a function which will convert the parameters from a given streaking type to another streaking type of the same module. For the sake of simplicity we just convert to the same parameters.

  @Override
  public double [] getAdjustedParameters (double [] params, int fromType, int toType)
  {
    return copy(params);
  }

Before we go over to the streaking function, what is missing is the implementation of this little helper function.

  private double [] copy (double [] values)
  {
    double [] result = new double[values.length];
    System.arraycopy(values, 0, result, 0, values.length);
    return result;
  }

The last thing before we go to implement the streaking: Since we provide a negative based and a paper only based version and to allow for similar strength values in both cases, we need to know the range of negative values to compensate for that in the paper only case. So here is a function which will compute the maximum negative value:

  private static double getMax (double[][] negative, int width, int height)
  {
    double max = 0f;
    for (int x = 0; x < width; x++)
      for (int y = 0; y < height; y++)
        if (negative[x][y] > max)
          max = negative[x][y];
    return max;
  }

After having dealt with all the logistics of the module, here is the implementation of the modules work horse. First its skeleton to say a few words about the parameters.

  @Override
  public void doStreaking(double[][] negative, double[][] pattern,
                          Dimension dimension, Dimension realDimension,
                          Point cropSource, Random random, Progress progress,
                          LogWriter logWriter, double[] parameters, int streakingType)
  {
    ...
  }

Let us have a closer look on the parametes:

Now let's jump to the implementation.

First print a log message to the log file and then we check the parameters, if not valid then return right away.

    logWriter.log(LogWriter.LogLevel.INFO, "Streaking Module Demo called.");
    double [] params = this.validateParameterValues(parameters);
    if (params[STRENGTH] == 0f)
      return;

Next look at the number of lines and compute them according to the given parameters if it is a relative type.

    double numHorizontal = params[NUM_H];
    double numVertical = params[NUM_V];
    if (streakingType == RELATIVE_TYPE_ID || streakingType == RELATIVE_PO_TYPE_ID) {
      numHorizontal = height/100f*numHorizontal;
      numVertical = width/100f*numVertical;
    }
    double strength = params[STRENGTH];
    boolean paperOnly = streakingType == ABSOLUTE_PO_TYPE_ID || 
                        streakingType == RELATIVE_PO_TYPE_ID;

Since we have horizontal and vertical directions we need to make sure that the progress is increasing from 0 to 100.

    double progressFactor = 150f;
    int progressOffset = 0;
    if (numHorizontal > 0) {
      progressFactor = progressFactor-50f;
      progressOffset = 50; // for the vertical part
    }
    if (numVertical > 0)
      progressFactor = progressFactor-50f;

Next we compensate for the paper only case:

    double exp = getMax(negative, dimension.width, dimension.height) / 2f

Now we are ready to go. If the number of horizontal lines is not zero, then handle the horizontal lines. We compute the delta between two lines and then walk the image in delta steps from top to bottom. We also update the progress of the task.

    if (numHorizontal > 0) {
      int deltaY = (int)(dimension.height/numHorizontal);
      if (deltaY == 0)
        deltaY = 1;
      int y = deltaY/2;
      while (y < dimension.height) {
        progress.update((int)((progressFactor*y)/dimension.height));
        ...
        y += deltaY;
      }
      progress.update(100);
    }

For each line to handle we add for each pixel a random amount of exposure and depending on paper only or not we do that while looking at the negative too or not.

        for (int x = 0; x < dimension.width; x++) {
          double p;
          if (paperOnly)
            p = pattern[x][y] + random.nextDouble() * exp * strength;
          else
            p = pattern[x][y] + random.nextDouble() * negative[x][y] * strength;
          if (p > 1f)
            pattern[x][y] = 1f;
          else
            pattern[x][y] = p;
        }

And to make the example complete, here is the handling of vertical lines:

    if (numVertical > 0) {
      int deltaX = (int)(dimension.width/numVertical);
      if (deltaX == 0)
        deltaX = 1;
      int x = deltaX/2;
      while (x < width) {
        progress.update((int)((progressFactor*x)/dimension.width)+progressOffset);
        for (int y = 0; y < dimension.height; y++) {
          double p;
          if (paperOnly)
            p = pattern[x][y] + random.nextDouble() * exp * strength;
          else
            p = pattern[x][y] + random.nextDouble() * negative[x][y] * strength;
          if (p > 1f)
            pattern[x][y] = 1f;
          else
            pattern[x][y] = p;
        }
        x += deltaX;
      }
      progress.update(100);
    }

If you do not want to type all that code on your own, feel free to download the java file: StreakingDemo.java

Shortcut StreakingModuleBase

The above example shows the full implementation of a streaking module. But there is a shortcut which avoids almost all of the logistics methods. Instead of just implementing the StreakingModule interface, you can also extend the StreakingModuleBase class and then override what is needed. Then the implementation might look like:

package de.zonev.demo;

import java.util.Random;
import java.util.List;
import java.util.ArrayList;
import de.zonev.digitalLith.sdk.StreakingModule;
import de.zonev.digitalLith.sdk.StreakingModuleBase;
import de.zonev.digitalLith.sdk.Progress;
import de.zonev.digitalLith.sdk.LogWriter;

public class DemoStreaking2 extends StreakingModuleBase implements StreakingModule
{
  private static final int MODULE_ID = 90100;

  private static final int ABSOLUTE_TYPE_ID = MODULE_ID;
  private static final int RELATIVE_TYPE_ID = MODULE_ID+1;
  private static final int ABSOLUTE_PO_TYPE_ID = MODULE_ID+2;
  private static final int RELATIVE_PO_TYPE_ID = MODULE_ID+3;

  private static final String MODULE_NAME = "Demo Streaking 2";
  private static final String MODULE_VERSION = "1.0";
  
  private static final String [] PROVIDED_TYPE_NAMES = new String [] {
    "Demo 2 (absolute)", "Demo 2 (relative)", "Demo 2 Paper Only (absolute)", "Demo 2 Paper Only (relative)"
  };

  private static final int [] PROVIDED_TYPE_IDS = new int [] {
    ABSOLUTE_TYPE_ID, RELATIVE_TYPE_ID, ABSOLUTE_PO_TYPE_ID, RELATIVE_PO_TYPE_ID
  };

  private static final String [] TYPE_DESCRIPTIONS = new String [] {
    "Demo streaking provides a line kind of streaking",
    "Demo streaking provides a line kind of streaking"
  };

  private static final String [] PARAMETER_NAMES = new String [] {
    "Number Horizontal", "Number Vertical", "Strength"
  };

  private static final String [] PARAMETER_NAMES_WITH_ABSOLUTE_UNITS = new String [] {
    "Number Horizontal (positive number)", "Number Vertical (positive number)", "Strength (>=0, <=1)"
  };
  
  private static final String [] PARAMETER_NAMES_WITH_RELATIVE_UNITS = new String [] {
    "Number Horizontal (%)", "Number Vertical (%)", "Strength (>=0, <=1)"
  };

  private static final String [] PARAMETER_ENCODING_NAMES = new String [] {
    "horizontal", "vertical", "strength"
  };

  private static final String [] TOOL_TIPS = new String [] {
    "This is the number of horizontal lines to be added. It is either a percentage (relative case) " +
    "or an absolute nuber (absolute case).",
    "This is the number of vertical lines to be added. It is either a percentage (relative case) " +
    "or an absolute nuber (absolute case).",
    "This is the strength of the effect. It is a number between 0 and 1 and comparable with the grain parameter"
  };

  private static final double [] PARAMETER_DEFAULTS = new double [] {
    20f, 0f, 0.1f      
  };

  private static final int NUM_H = 0;
  private static final int NUM_V = 1;
  private static final int STRENGTH = 2;
  ...
}

Now create a default constructor which calls the parents constructor to fill the data. Here we give a null for the parameter names with units since we have different values for the provided streaking types and so have to override the function to get the parameter names with units anyway.

  public StreakingDemo2 ()
  {
    super(MODULE_ID, MODULE_NAME, MODULE_VERSION, PROVIDED_TYPE_IDS, PROVIDED_TYPE_NAMES,
          TYPE_DESCRIPTOINS, PARAMETER_NAMES, null, PARAMETER_ENCODING_NAMES, TOOL_TIPS, PARAMETER_DEFAULTS, true);
  }

Then you get an implementation of most of the methods from StreakingModule for free. All you have to provide is the validate and flip horizontal methods as well as the method to get the parameter names with units. Here we also use the flip and copy methods provided by the base class for your convenience.

  @Override
  public String[] getParameterNamesWithUnits(int streakingType)
  {
    if (streakingType == ABSOLUTE_TYPE_ID || streakingType == ABSOLUTE_PO_TYPE_ID)
      return PARAMETER_NAMES_WITH_ABSOLUTE_UNITS;
    else
      return PARAMETER_NAMES_WITH_RELATIVE_UNITS;
  }

  @Override
  public List validateParameters (int streakingType, double [] params)
  {
    List invalidParameters = new ArrayList();
    checkGreaterEqualZero(STRENGTH, params, invalidParameters);
    checkGreaterEqualZero(NUM_H, params, invalidParameters);
    checkGreaterEqualZero(NUM_V, params, invalidParameters);
    return invalidParameters;
  }

Please note in the function above we use some helper functions from the base class.

  @Override
  public double[] validateParameterValues(double[] values)
  {
    double [] result = copy(values);
    if (result[NUM_H] == 0f && result[NUM_V] == 0f)
      result[STRENGTH] = 0f;
    else if (result[STRENGTH] == 0f) {
      result[NUM_H] = 0f;
      result[NUM_V] = 0f;
    }
    return result;
  }

  @Override
  public double[] flipHorizontalAndVertical(double[] values)
  {
    double [] result = copy(values);
    flip(result, NUM_V, NUM_H);
    return result;
  }

  @Override
  public boolean isUsableParameter(double[] values)
  {
    double [] v = validateParameterValues(values);
    return v[STRENGTH] != 0f;
  }

And then have the same doStreaking implementation as above. Except that we use the getMax method provided by StreakingModuleBase.

  @Override
  public void doStreaking(double[][] negative, double[][] pattern,
                          Dimension dimension, Dimension realDimension,
                          Point cropSource, Random random, Progress progress,
                          LogWriter logWriter, double[] parameters, int streakingType)
  {
    ...
  }

That is it. And here you can find the java code to download: StreakingDemo2.java

Building and validating the module

How you build the module, if doing it by hand or using a make tool like ant or some IDE is up to you. All you have to make sure is that your module only contains your classes and that the manifest file of the modules jar file does define which digital lith modules it contains.

For simplicity here are the manual steps to build your module.

Let's say here is how it looks on disk:

DemoStreaking/
     +---------> src/
     |            +---------> de/
     |                         +---------> zonev/
     |                                       +----------> demo/
     |                                                      +----------> StreakingDemo.java
     +---------> manifest.mf
     +---------> digitallith_sdk_4.1.0/
                       +------------> digitallith_sdk.jar
                       +------------> digitallith_sdktester.jar

First we compile the sources:

mkdir classes
javac -classpath digitallith_sdk_4.1.0/digitallith_sdk.jar -d classes src/de/zonev/demo/StreakingDemo.java

To build the jar we need the manifest.mf file. Here is how it looks like, just one single line:

DigitalLithStreakingModulesV41: de.zonev.demo.StreakingDemo

If you provide more than one module in a single jar file, then just add them as comma-separated list like

DigitalLithStreakingModulesV41: de.zonev.demo.StreakingDemo, de.zonev.demo.StreakingDemo2

Now let us build the jar file:

mkdir dist
cd classes
jar cvmf ../manifest.mf ../dist/StreakingDemo.jar *

That's it! You just built your first own streaking module. Now put it into your extensions directory as set in the preferences and restart Digital Lith.

But wait, you may want to validate it first. Just to be sure that everything is OK with your module. This is also a good idea if you get a module from somewhere else or if you installed your module, but it does not show up in the streaking types of the digital lith UI.

There is a validation tool included in the sdk. It is a jar file that you just use from the command line. The parameters to provide are the jar file of your module, the class file of your module.

java -jar digitallith_sdk_4.1.0/digitallith_sdktester.jar dist/StreakingDemo.jar de.zonev.demo.StreakingDemo

In the case of the demo module that might look like the following:

ModuleTester: No errors found.

But if your number of tool tips defined for the parameters differ from the number parameters defined, then the tester will print a warning and stop validating.

Streaking Module Validation Error: Parameter names and tool tips differ in length.

OK, now you are ready to place the module in your extensions directory and restart digital lith.

Here is what it would look like:

Streaking Demo Preview

An Example Process Module

Since Digital Lith Processing V4 there is the possibility to create your own digital lith process. This is as simple as providing a streaking module and only requires implementation of a simple Java interface and building a jar file. Put the jar file into the extensions directory and restart digital lith. Now you will find a selection box which lets you choose your own processing instead of the Digital Lith Standard process. There will also be different process modules available on the Digital Lith Exchange.

We now are going to implement a very simple demo process module. It will come with only a few parameters and the computation will be very simple. But it will show how you can do your own process modules.

We are going to use the ProcessModule interface and derive from the provided ProcessModuleBase class.

The Java Code

Here is the skeleton for the DemoProcess implementation:

package de.zonev.demo;

import java.util.Random;
import java.awt.Dimension;
import java.awt.Point;
import de.zonev.digitalLith.sdk.ProcessModule;
import de.zonev.digitalLith.sdk.ProcessModuleBase;
import de.zonev.digitalLith.sdk.Progress;
import de.zonev.digitalLith.sdk.LogWriter;

public class DemoProcess extends ProcessModuleBase implements ProcessModule
{
   ...
}

Now lets subsequently add all the parts needed for the process module.

First we define some constants which we are going to feed into the constructor of the base class later.

We start with the module definition which is a process id, name and description.

  public static final int PROCESS_ID = 99999;
  public static final String PROCESS_NAME = "Demo Process";
  public static final String PROCESS_DESC = "A Demo Process implementation";

Now we use some constants for the parameter positions which will make life a little bit easier.

  public static final int DILUTION = 0;
  public static final AREA = 1;
  public static final GRAIN = 2;

Then we go on with the parameter definitions. These are parameter names, the names with units (when selected to show the units in the UI), their encoding, a description and default values. We go with three parameters for the demo module, a dilution, the radius of the disc used for the local effects of the processs and the amount of grain. All three parameters are known from the standard process and have similar effect.

  public static final String [] CUSTOM_PARAM_NAMES = new String [] {
    "Dilution", "Area", "Grain"
  };
  public static final String [] CUSTOM_PARAM_NAMES_WITH_UNITS = new String [] {
    "Dilution (positive number)", "Area (0,1,...)", "Grain >=0, <= 1)"
  };
  public static final String [] CUSTOM_PARAM_ENCODING = new String [] {
    "dilution", "area", "grain"
  };
  public static final String [] CUSTOM_PARAM_DESC = new String [] {
    "The developer dilution", "The radius of the disc taken when doing the next iteration", "Amount of grain injection"
  };
  public static final double [] CUSTOM_PARAM_DEFAULTS = new double [] {
    50f, 2f, 0.1f
  };

Next we go on with a default constructor with initializes the base class with the constants we just defined. You can go without extending the base class, but then you would have to do all that manual work yourself.

  public DemoProcess ()
  {
    super(PROCESS_ID, PROCESS_NAME, PROCESS_DESC, CUSTOM_PARAM_NAMES, CUSTOM_PARAM_NAMES_WITH_UNITS,
          CUSTOM_PARAM_ENCODING, CUSTOM_PARAM_DESC, CUSTOM_PARAM_DEFAULTS);
  }

The process itself is a multi-step process and so we need to pass data from step to step. For that reason we define our own custom data class. During initialization we setup an object of that class and then this object is handed into the step methods.

In our custom data object we want to store the grain and dilution used as well as a mask used to compute the local effect and the width and height for convenience since then we do not have to get it from the array sizes.

  public class DemoProcessData
  {
    double dilution;
    double [][] mask;
    double grain;
    int width;
    int height;
  }

As with the streaking modules there is a validation method for invalid parameters. It makes use of some helper functions provided by the base class.

  @Override
  public List validateParameters(double[] customParameters)
  {
    List invalidParameters = new ArrayList();
    checkGreaterZero(DILUTION, customParameters, invalidParameters);
    checkInteger(AREA, customParameters, invalidParameters);
    checkGreaterEqualZero(AREA, customParameters, invalidParameters);
    checkGreaterEqualZero(GRAIN, customParameters, invalidParameters);
    return invalidParameters;
  }

Next is the preparation method which is called to setup the custom data object. It gets the custom parameters the dimension of the image to be computed, the real dimension of the source image and the crop source if this is a cropped preview as well as a log writer to write out errors and warnings.

If you do a test development the dimension will be the dimension of the preview image and crop source will be null if the test development is not of a cropped preview. Image root (0,0) is at the top left as is the cropSource the coordinates of the top left pixel of the crop area. From this data it is possible to use adjusted parameters to better reflect the effect on the final image.

In this example we do not make use of that.

  @Override
  public Object prepare(double[] customParameters, Dimension dim, Dimension realDim,
                               Point cropSource, LogWriter logWriter)
  {
    DemoProcessData result = new DemoProcessData();
    result.dilution = customParameters[DILUTION];
    result.mask = this.getWeightedMask((int)customParameters[AREA]);
    result.grain = customParameters[GRAIN];
    result.width = dim.width;
    result.height = dim.height;
    return result;
  }

Next is the expose step. Exposure is a multi-step process. The expose method is called twice in the process. The first time is right after initialization (step=1) the second time is after uneven development has been applied (step=2). If your process supports more than one step then overrid method getNumExposeSteps to return more than 1.

The parameters are the step num, the lith paper array, the negative array, the custom process data object from the prepare step, a random number generator, a progress object to report progress to the caller (used when doing batch processing) and a log writer to log errors and warning.

For the demo process what we do is to lay down a grain pattern to the paper and depending on the dilution add some part of the negative.

  @Override
  public void expose(int step, double[][] lithPaper, double[][] negative, Object customProcessData,
                     Random random, Progress progress, LogWriter logWriter)
  {
    if (step != 1)
      return;
    DemoProcessData processData = (DemoProcessData)customProcessData;
    int currentProgress = -1;
    for (int x = 0; x < processData.width; x++) {
      int newProgress = (int)((x*100f)/processData.width);
      if (newProgress != currentProgress) {
        currentProgress = newProgress;
        progress.update(newProgress);
      }
      for (int y = 0; y < processData.height; y++)
        lithPaper[x][y] = (negative[x][y] + random.nextDouble() * processData.grain) / processData.dilution;
    }
  }

Next are two helper functions, first is getMinimumExposureValue which is used to determine a good grain pattern for parts of the image which get negative uneven development and therefore have a value below 0. In that case we use a value returned from this function instead of 0.

Second is a function being used to adjust custom data after every iteration. In the standard process this is to adjust dilution with the dilute factor once the dilution step is reached. In our case it does: nothing.

  @Override
  public double getMinumumExposureValue (Object customProcessData, Random random)
  {
    DemoProcessData processData = (DemoProcessData)customProcessData;
    return random.nextDouble() * processData.grain;
  }

  @Override
  public void adjustForIteration(int iteration, Object customProcessData)
  {
    // do nothing here
  }

And now here is the heart of the process, the develop step. To support parallel execution, this module only works on one single line of the paper.

Parameters are the iteration step, the new paper to be created, the current paper, the negative, the custom process data, a random number generator, the line to work on and a log writer. It returns true if at least one pixel got totally black (≥1).

In the demo process we use the base class getAroundMask to get a value from the local area of the pixel we want to compute and add this to the existing paper value. We do not use the negative in this step.

  @Override
  public boolean doDevelop(int iteration, double[][] newPaper, double[][] lithPaper, double[][] negative,
                                  Object customProcessData, Random random, int y, LogWriter logWriter)
  {
    DemoProcessData processData = (DemoProcessData)customProcessData;
    boolean blackReached = false;
    for (int x = 0; x < processData.width; x++) {
      double p = lithPaper[x][y] + this.getAroundMask(lithPaper, processData.width, processData.height, x, y,
                                                     processData.mask, true) / processData.dilution;
      if (p > 1f) {
        newPaper[x][y] = 1f;
        blackReached = true;
      }
      else
        newPaper[x][y] = p;
    }
    return blackReached;
  }

That is all the source for the process module. And here you can find the java code to download: DemoProcess.java

Now the sources are complete, Let's find out how to create the module.

Building and validating the module

How you build the module, if doing it by hand or using a make tool like ant or some IDE is up to you. All you have to make sure is that your module only contains your classes and that the manifest file of the modules jar file does define which digital lith modules it contains - all very similar to the streaking module build process.

For simplicity here are the manual steps to build your module.

Let's say here is how it looks on disk:

DemoStreaking/
     +---------> src/
     |            +---------> de/
     |                         +---------> zonev/
     |                                       +----------> demo/
     |                                                      +----------> DemoProcess.java
     +---------> manifest.mf
     +---------> digitallith_sdk_4.1.0/
                       +------------> digitallith_sdk.jar
                       +------------> digitallith_sdktester.jar

First we compile the sources:

mkdir classes
javac -classpath digitallith_sdk_4.1.0/digitallith_sdk.jar -d classes src/de/zonev/demo/DemoProcess.java

To build the jar we need the manifest.mf file. Here is how it looks like, just one single line:

DigitalLithProcessModulesV41: de.zonev.demo.DemoProcess

If you provide more than one module in a single jar file, then just add them as comma-separated list like

DigitalLithProcessModulesV41: de.zonev.demo.DemoProcess, de.zonev.demo.AnotherDemoProcess

Now let us build the jar file:

mkdir dist
cd classes
jar cvmf ../manifest.mf ../dist/DemoProcess.jar *

That's it! You just built your first own process module. Now put it into your extensions directory as set in the preferences and restart Digital Lith.

As with the streaking modules you can also validate your process modules. And it is the very same module tester as with streaking.

java -jar digitallith_sdk_4.1.0/digitallith_sdktester.jar dist/DemoProcess.jar de.zonev.demo.DemoProcess

In the case of the demo module that might look like the following:

ModuleTester: No errors found.

Now you are ready to go. Put the module into the extension directory and restart digital lith. You should be able to select your module from the process module list.

Disclaimer and Copyright

DigitalLithProgramming is copyrighted by Ruediger Stobbe. You can use it for free at your own risk. The same is for the SDK.

Contact

Feedback is always welcome. So if you have suggestions about what is missing or about bugs - please feel free to contact me via email: rst@zonev.de

Good news is also welcome, so if you like the program ... let me know too.