Introduction
In part 1, we covered the very basics of external development - in particular how to setup, compile and debug an external on Mac OS X and Windows. While we did indeed
produce an external it wasn't anything particularly useful or interesting.
This time, we will delve further into external development by introducing more of
the Revolution externals API and then use this new knowledge to explore a more substantial
example - a basic image effect framework.
Prerequisites
These are mostly the same as that required by the first part - if you missed that
one you can read it here
http://www.runrev.com/newsletter/november/issue13/newsletter5.php. In addition:
Note: The only difference between ExternalsEnvironmentV2 and ExternalsEnvironmentV1
is that the former contains an updated external.h header file with the
previously promised API documentation there-in.
*VariableEx calls - the Swiss-army knife of the externals API
In our rnaHello external we created in part 1, we saw how to use the API
calls GetVariable and SetVariable. These calls are fine if you
wish to manipulate small amounts of text data, but because they operate using NUL-terminated
strings, they are completely useless if you want to work with binary data. Furthermore,
they give no way of accessing variables that might be arrays. Therfore, to deal
with such cases, it is necessary to become familiar with a related pair of functions
GetVariableEx and SetVariableEx.
This pair of functions have three advantages over their simpler cousins:
The value you pass in and out is a pair consisting of a data pointer and a length
- i.e. there is no reliance on a NUL-terminator to determine the length of the data.
You can specify a key, enabling you to access values of individual elements of the
array.
The data returned by GetVariableEx is not copied, resulting in
a performance gain when fetching large blocks of data (and you don't have to
worry about free'ing anything).
The syntax of these functions is a natural extension of GetVariable and
SetVariable:
void GetVariableEx(const char *p_name, const char *p_key, ExternalString *r_value,
int *r_success)
void SetVariableEx(const char *p_name, const char *p_key, const ExternalString *p_value,
int *r_success)
Notice here that there is an extra p_key parameter and rather than the
value being a char *, it is now an ExternalString *.
Manipulation of binary data
As mentioned above, one of the advantages of this pair of functions is that they
allow you to work with arbitrary binary data. This comes about because a value is
passed and returned as a pointer to an ExternalString structure. This structure
is defined with two members:
struct ExternalString
{
const char *buffer;
int length;
}
Where buffer is a pointer to the data, and length is its length
in bytes. For example to get the value of a variable called tBinaryData
one would do something like:
ExternalString t_value;
GetVariableEx("tBinaryData", "", &t_value, &t_success);
And to set the value of a variable called tBinaryData to some pre-existing
value, one would do something like:
ExternalString t_value;
t_value . buffer = (const char *)t_my_buffer;
t_value . length = t_my_buffer_length;
SetVariableEx("tBinaryData", "", &t_value, &t_success);
Here, you will notice we pass the empty string ("") as the second argument - the
p_key parameter. This tells the engine to return the value of the given
variable as if it were a normal (non-array) variable.
One thing to be wary of when using these forms is that the data you get back by
using GetVariableEx will not (in general) be NUL terminated, even if it is 'text'
in some sense. Importantly this means you should take great care when using the
standard C string manipulation routines.
Delving into arrays
The other main use of VariableEx function pair is to access elements of a Revolution
array variable. To do this, simply pass the name of the key you wish to access in
the p_key parameter. For example, if you want to get element foo
of variable tBar one would do something like:
ExternalString t_value;
GetVariableEx("tBar", "foo", &t_value, &t_success);
Or if you want to get element n of variable tBaz, you would need to do
something akin to:
char t_key[16];
sprintf(t_key, "%d", n);
GetVariableEx("tBaz", t_key, &t_value, &t_success);
Here you are seeing a side-effect of Revolution arrays really being hash-tables
with string-based keys - even numeric based arrays require you to explicitly convert
the index to a string before fetching.
One question you might be caused to ask is - what happens if I try to get or set
an element that doesn't exist? The answer is exactly the same as what happens when
you try to do that in Revolution: in the case of setting, the element gets created;
in the case of getting you get empty back (in this case empty
is a buffer of length 0). In particular, r_success only reports EXTERNAL_FAILURE
if the variable itself does not exist.
A word of caution
As the third benefit of the VariableEx calls, we mentioned the increased efficiency
gained by the data not being copied when the external fetches it from Revolution.
This is all well and good, but does come with a small word of warning: any changes
to the variable, or the referenced variable's element will cause the pointer that
is returned to become invalid.
For example, the following code is likely to cause a crash:
ExternalString t_original_value;
GetVariableEx("tBar", "", &t_original_value, &t_success);
SetVariable("tBar", "Hello World!", &t_success);
if (t_original_value . length == 5 && memcmp(t_original_value . buffer,
"hello", 5) == 0)
printf("It starts with hello");
This is because the engine will free the memory referenced in t_original_value
during the course of the SetVariable call, and so when a pointer to it is used in
the memcmp call there is no guarantee it will still be valid.
However, don't get too concerned about this. If you keep to the general principal
of fetching input from input variables, doing the necessary processing and then
writing back the results to the output variables it is unlikely that you will end
up with any dangling pointers.
Case-Study: an image-effect framework
So far this article has been somewhat dry and it is about time we got our hands
dirty. Rather than produce a small and essentially useless external just to exhibit
the introduced APIs, we will instead explore a slightly more meaty example of an
external.
Background
What we will be producing in the remainder of this article is a basic image effect
framework - an external with a simple API allowing you to apply effects to the image
and alpha data of images. While simple, the framework has been designed to be easily
extensible, requiring little more than implementing the algorithm that actually
implements the effect you want.
The framework exports just two external handlers:
rnaEffectList - an external function returning a list of all known effects
rnaEffectApply - an external command taking details of an effect and an
input image, and outputing a processed image
The external makes extensive use of both the array and binary manipulation features
of the externals API. Indeed, from the point of view of Revolution, effects
and images are defined by arrays having certain sets of keys. The external
then uses these arrays to dispatch to the appropriate effect and provide it with
the correct data.
The framework is written using C++ and makes use of both abstract base classes for extensibility, and exceptions for implicit error-handling.
Getting Started
Before continuing we need to setup a new external. So, make sure you have an unpacked
Externals Environment (the one from the last article will do) and load
up the External Creator. Then, generate a new external with the following
settings:
When this has been generated, unpack the NewsletterArticle2.zip archive
and copy the source files from within the rnaeffect_skeleton/src folder into
the rnaEffect/src folder inside your externals environment. The files you
have just copied is the framework itself albeit without any implemented effects.
(Don't worry about overwriting the 'rnaeffect.cpp' file, you want the one from the
archive - it actually does something!).
Now load the new project into XCode or Visual C++, depending on your current platform.
The next thing to do is to add the auxillary files to the project. This can be done
in one of several ways - the easiest is probably by using drag-drop. Select all
the source files (both CPP and H) in the src folder and drag them onto the project.
In Visual C++, you should drag them onto the rnaEffect project node.
In XCode, drag them into the Source node under the rnaEffect
node and just choose Add when you get prompted with a dialog asking you
which targets to add them to. Be careful you don't get a duplicate rnaeffect.cpp
node - if you do, just choose one, select Edit -> Delete, and then Delete References.
These new files serve the following purposes:
rnaeffect.cpp - this contains the main implementation of the external, containing
two external handler definitions
effect.cpp/effect.h - this pair of files define an abstract class Effect
which you can inherit from in order to implement an effect
variable.cpp/variable.h - this pair of files define an abstraction of a Revolution
variable making them easier to work with (see the next section)
utility.cpp/utility.h - this pair of files declare a number of utility functions
and classes making development easier
It is probably now worth taking a brief look through the files in the project. Most
source files have been heavily commented to help explain what they are for. In particular
have a look through the implementation of the external handlers in rnaeffect.cpp.
Abstracting variable access
If you have taken a look through rnaeffect.cpp then you will notice that
there is not a single mention of SetVariableEx or GetVariableEx.
Indeed, having introduced our friends GetVariableEx and SetVariableEx,
they have now been rendered obsolete by wrapping them in an abstraction - the Variable
class. At this point take a moment to browse through the Variable.h
header file.
The idea of this class is simple - you declare a variable of type Variable
and pass a variable name to its constructor. This object is then (notionally) linked
to the Revolution variable and you can use a rich collection of methods to manipulate
its value.
For example, to fetch the value of a Revolution variable call tBar as an
integer you would do:
Variable t_bar("tBar");
int t_value;
t_value = t_bar . GetInteger();
Or, to set the value of element foo of variable tBaz to the string
"Hello World!" you would do:
Variable t_baz("tBaz");
t_baz . SetCStringElement("foo", "Hello World!");
As you can see, this significantly simplifies fetching and storing variable values
particularly as any required type conversions are done for you by the wrapper class.
One point worth mentioning is that this version of the Variable class relies on
exceptions to handle errors. i.e. If a variable doesn't exist, or doesn't contain
a string that can be coerced to the requested type an exception will be thrown.
Being just a 'wrapper' class, Variable's implementation is straightforward and those
of you interested in the details may want to take a good look through the Variable.cpp
class to see how it works. (It really is nothing more than a thin wrapper around
the SetVariableEx and GetVariableEx calls).
Introducing effects
An effect is a class derived from the abstract class
Effect. The Effect class has two pure virtual methods - Calculate
and Apply - that must be implemented. In addition, a means by which to create an
instance of the effect from a Revolution array must be provided.
The Calculate method is used by the framework to work out how big an output
image will be given an input image of a given size. It has signature:
void Calculate(unsigned in p_in_width, unsigned int p_in_height,
unsigned int& r_out_width, unsigned int& r_out_height)
The implementation must use p_in_width and p_in_height together with any parameters
passed to the effect when it was created to compute the output size which should
then be stored in r_out_width and r_out_height.
The Apply method is the workhorse of any effect, actually performing the
effect's operation on an input image and putting the output into an output image.
It has signature:
void Apply(const Image& p_input, Image& p_output)
From the point of view of the framework, an Image is a quadruple:
struct Image
{
unsigned int width;
unsigned int height;
unsigned char *color;
unsigned char *alpha;
};
A structure containing the images width and height in pixels, together with pointers
to memory buffers containing the color (pixel) and alpha (mask) data. These data
buffers are in the same format as the imageData and alphaData
properties of a Revolution image.
The framework takes care of fetching the input data and allocating the output buffers,
so all the Apply method needs to do is take the input and write it to the
output after it has applied its magic.
Our first effect
Our first effect will be the 'no-op' effect. It will take an input image and map
it directly to the output image without changing it in any way. While not actually
useful, it is the simplest possible effect we could implement and will illustrate
how to add an effect perfectly.
The first thing we need to do is add the definition and implementation of the NopEffect
class to our project.
In Visual C++, click the right mouse-button on the rnaeffect node of the
project and choose Add -> New Item.... Choose 'C++ file (.cpp)', give it the
name nop_effect.cpp, and make sure it is being created in the src
folder (check the Location field). Repeat this again but choose 'Header file (.h)' and give it the name nop_effect.h.
In XCode, right click on the Source node in the project, choose New File...
and select Empty File in Project.
Give it the name nop_effect.cpp and make sure it is created in the src
folder. Repeat this again but add a file called nop_effect.h. (In
both cases, you do want it added to the rnaeffect target - which will be
the default setting).
Open up the nop_effect.h file and paste in the following:
#ifndef __NOP_EFFECT__
#define __NOP_EFFECT__
#ifndef __EFFECT__
#include "effect.h"
#endif
class NopEffect : public Effect
{
public:
NopEffect(const char *p_info_variable);
void Calculate(unsigned int p_in_width, unsigned int p_in_height,
unsigned int& p_out_width, unsigned int& p_out_height);
void Apply(const Image& p_input, Image& p_output);
};
#endif
This is nothing more than the declaration of our new class, derived from Effect.
Now in the nop_effect.cpp we need to define three methods - but first we
need to ensure we include the relevant definitions. At the top of the file place
the following includes:
#include <cstring>
#include "effect.h"
#include "nop_effect.h"
Follow this by the method implementations. First the constructor:
NopEffect::NopEffect(const char *p_info_variable)
{
}
Since our effect does nothing to any given image, it has no parameters and so the
constructor need not do anything.
Next, we need the Calculate method:
void NopEffect::Calculate(unsigned int p_in_width, unsigned int p_in_height,
unsigned int& p_out_width, unsigned int& p_out_height)
{
p_out_width = p_in_width;
p_out_height = p_in_height;
}
Again, our effect has no effect, so we just copy the input width and height into
the output width and height.
Finally, the main part - the Apply method:
void NopEffect::Apply(const Image& p_input, Image& p_output)
{
if (p_input . color != NULL)
memcpy(p_output . color, p_input . color, p_input . width * p_input . height * 4);
if (p_input . alpha != NULL)
memcpy(p_output . alpha, p_input . alpha, p_input . width * p_input . height);
}
Again, our effect does no processing so we just copy the input data to the output
data. Here it is worth noting two things. First, if either color or alpha data is
not present in the input (indicated by the buffer being NULL), it cannot be present
in the output. Second, the size of the buffers are one byte per pixel for alpha,
and four bytes per pixel for color - just the same as the corresponding data properties
of images in Revolution.
Having implemented the NopEffect class, all that remains is to hook it into the
framework. To do this, open up the effect.cpp file and locate the // BEGIN
USER EFFECT INCLUDES line. After this line put:
#include "nop_effect.h"
This will cause the declaration of our new effect to be imported. Then find the
// BEGIN USER EFFECTS line and after it put the entry:
{ "nop", EffectCreate<NopEffect> },
This line adds an entry to an array that tells the framework to create an instance
of the NopEffect class whenever an effect with name nop is requested.
Having done all of this, click Run (Visual C++), or Build followed by Debug (XCode)
and, all being well, an empty test stack should appear.
In Visual C++, make sure that rnaeffect is selected as your 'Startup project'.
Remember that in XCode, you need to do a release build before a debug build the first time a
project is compiled - otherwise when you debug, the paths will be wrong and the external won't
get loaded.
Using the framework
You should now be looking at a blank stack ready to start testing our effects framework
and new effect. First of all, let's check that our effect is recognized.
Switch to the Message Box and ensure that the rnaEffectTest stack is the current
target. Then execute put rnaListEffects(). You should see a list consisting
of one item nop (if you don't, retrace your steps and ensure that both
nop_effect.cpp and nop_effect.h are in the project, and that the s_effects
array in effect.cpp has a 'nop' entry).
As mentioned before, the framework works by using arrays to communicate. The rnaApplyEffect
external command takes three parameters:
pEffectInfoName - the name of an array variable describing the required
effect
pInputImageName - the name of an array variable describing the input image
pOutputImageName - the name of a variable to receive the output image description
The keys required in the effect array will depend on the effect, but at the very
least there should be one key call name containing the name of the effect
to invoke.
The array describing the input image requires the following keys:
width - the width of the image in pixels
height - the height of the image in pixels
color - the imageData of the image (optional)
alpha - the alphaData of the image (optional)
Similarly, the output image variable will contain an array with similar keys.
So to try out the nop effect, import a suitable image onto the test stack - any image will do - and call
it Input. Then add an additional empty image object (by dragging from the
tools palette) and call it Output. Finally, add a button called Nop
with the following
script:
on mouseUp
local tEffect
put "nop" into tEffect["name"]
local tInput
put the width of image "Input" into tInput["width"]
put the height of image "Input" into tInput["height"]
put the alphaData of image "Input" into tInput["alpha"]
put the imageData of image "Input" into tInput["color"]
local tOutput
rnaApplyEffect "tEffect", "tInput", "tOutput"
lock screen
set the width of image "Output" to tOutput["width"]
set the height of image "Output" to tOutput["height"]
set the alphaData of image "Output" to tOutput["alpha"]
set the imageData of image "Output" to tOutput["color"]
unlock screen
end mouseUp
Clicking the button should result in the output image turning out to be identical
to the input image! At this point save the test stack and quit Revolution.
An image adjustment effect
Now having implemented a completely useless effect, let us build something slightly
more interesting - a simple implementation of contrast and brightness adjustment.
This effect will be called adjust and take two parameters brightness
and contrast.
Add a pair of files to the project adjust_effect.cpp and adjust_effect.h.
Next, copy the following into the adjust_effect.h
#ifndef __ADJUST_EFFECT__
#define __ADJUST_EFFECT__
#ifndef __EFFECT__
#include "effect.h"
#endif
class AdjustEffect: public Effect
{
public:
AdjustEffect(const char *p_info_variable);
void Calculate(unsigned int p_in_width, unsigned int p_in_height,
unsigned int& p_out_width, unsigned int& p_out_height);
void Apply(const Image& p_input, Image& p_output);
private:
int m_brightness;
int m_contrast;
};
#endif
This defines another derivation of Effect. This time, though it has some
state - m_brightness and m_contrast. These two integral values
will be extracted from the effect array when the an AdjustEffect object
is created.
Next open up the adjust_effect.cpp file and start off by adding the following
includes:
#include <cstring>
#include "variable.h"
#include "effect.h"
#include "adjust_effect.h"
After this we need our three methods. First the constructor:
AdjustEffect::AdjustEffect(const char *p_info_variable)
{
Variable t_info(p_info_variable);
m_contrast = t_info . GetIntegerElement("contrast");
m_brightness = t_info . GetIntegerElement("brightness");
}
Here, we first wrap the info variable name in a Variable object, then fetch
two integer values from the keys contrast and brightness. These
values are stored in the private state of the AdjustEffect object so the
other two methods have access to them.
Next, we need the Calculate method:
void AdjustEffect::Calculate(unsigned int p_in_width, unsigned int p_in_height,
unsigned int& p_out_width, unsigned int& p_out_height)
{
p_out_width = p_in_width;
p_out_height = p_in_height;
}
As before, this effect does not change the size of the image and so we just copy
input size to output size.
Finally, we need the implementation. As previously mentioned, the image data is
made available to the Apply method as (up to) two pointers to memory buffers.
These buffers contain data in exactly the same format as Revolution image and alpha
data properties - that is one byte-per-pixel for alpha data and four bytes-per-pixel
for image data (the latter being in sequence pad, red, green, blue for each pixel).
Therefore, applying an effect is a simple matter of looping over the input data
in an appropriate way, and then writing the processed data to the provided output
buffer.
In this case, we will be applying the following function to the red, green and blue
components of each pixel:
new_value = min(0, max(255, ((value - 128) * contrast) / 128 + 128 + brightness))
This is nothing more than a simple-minded contrast and brightness adjustment - common
in many image-processing applications.
Therefore, we can use something like the following to achieve our desired effect:
void AdjustEffect::Apply(const Image& p_input, Image& p_output)
{
// Process the color data
//
if (p_input . color != NULL)
{
const unsigned char *t_in_ptr;
t_in_ptr = p_input . color;
unsigned char *t_out_ptr;
t_out_ptr = p_output . color;
// Since this effect is not sensitive to row or column, we just iterate
// through each pixel without regard for its x or y location.
//
for(unsigned int p = 0; p < p_input . width * p_input . height; ++p)
{
// Skip the pad byte in both input and output
t_in_ptr++;
t_out_ptr++;
// Since this effect is not sensitive to channel, we just iterate
// through 3 channels for each pixel - red, green and blue.
//
for(unsigned int c = 0; c < 3; ++c)
{
int t_in_value;
t_in_value = *t_in_ptr++;
int t_out_value;
t_out_value = ((t_in_value - 128) * m_contrast) / 128 + 128 + m_brightness;
if (t_out_value < 0)
t_out_value = 0;
else if (t_out_value > 255)
t_out_value = 255;
*t_out_ptr++ = t_out_value;
}
}
}
// We have no effect on the alpha data, so we just copy it straight
// through.
//
if (p_input . alpha != NULL)
memcpy(p_output . alpha, p_input . alpha, p_input . width * p_input . height);
}
With the adjust_effect.cpp file finished, all that remains is to add:
#include "adjust_effect.h"
to the appropriate place in effect.cpp. Followed by an entry in the effects
table in the same file:
{"adjust", EffectCreate<AdjustEffect> },
Now rebuild and run the project.
Using the
adjust effect
Let us now hook into our new effect.
First of all, to the test stack add two sliders: one Brightness with range
-127 to 127; and one Contrast with range 0 to 255. Then create a new button
Adjust and give it the following script:
on mouseUp
local tEffect
put "adjust" into tEffect["name"]
put the thumbPosition of scrollbar "Brightness" into tEffect["brightness"]
put the thumbPosition of scrollbar "Contrast" into tEffect["contrast"]
local tInput
put the width of image "Input" into tInput["width"]
put the height of image "Input" into tInput["height"]
put the alphaData of image "Input" into tInput["alpha"]
put the imageData of image "Input" into tInput["color"]
local tOutput
rnaApplyEffect "tEffect", "tInput", "tOutput"
lock screen
set the width of image "Output" to tOutput["width"]
set the height of image "Output" to tOutput["height"]
set the alphaData of image "Output" to tOutput["alpha"]
set the imageData of image "Output" to tOutput["color"]
unlock screen
end mouseUp
Now try playing around with the sliders and clicking Adjust - you should
see a familiar effect!
Building
on the framework
The final code for the framework and effects decribed here can be found in the rnaeffect_final
folder in the NewsletterArticle2.zip archive.
Next time...
This time round we have explored two of the most important calls in the externals
API and used them to create a basic image effect framework enabling the simple creation
of image processing filters - a task ideally suited to native code. In the next
part of the series we will start focusing on more platform-specific development
- starting by looking at how to use Revolution provided windowId's effectively with
OS APIs.
|