From shf@netcom.com Date: Fri, 6 Jan 1995 19:13:05 -0800 From: Stuart Ferguson Reply to: lwplugin-l@netcom.com To: lwplugin-l@netcom.com Subject: LWSDK: flat text docs LightWave Plug-in Architecture -- Stuart Ferguson 1/6/95 1 Plug-In Interface 1.1 Server Identification 1.2 Server Activation Function 1.3 The Global Function 1.4 Plug-in and Built-in Servers 2 Server Interface 2.1 Plug-in Initialization and Cleanup (1) Startup usage (2) Shutdown usage 2.2 Activation Function (3) Activation function args (4) ActivateFunc type 2.3 Global Function (5) GlobalFunc types (6) GlobalFunc types 2.4 The Global Server Class (7) Global activation data 2.5 External Function Entry Points (8) XCALL Definitions 2.6 Single-Service Plug-ins (9) Activate usage 2.7 Multiple-Service Plug-ins (10) ServerRecord type 3 Example Plug-in Service 3.1 String Transform Class (11) Test types (12) Test types 3.2 String Transform Functions (13) String transform arguments 3.2.1 Length Operation (14) Length function body 3.2.2 Reverse Operation (15) Reverse function body 3.2.3 Capitalization Operation (16) Capitalize function body 3.2.4 Double Operation (17) Double function body 3.3 Implementing Servers 3.3.1 Single-Service Plug-in -- Reverse (18) Test Reverse plug-in program 3.3.2 Multiple-Service Plug-in -- Caps & Double (19) Test Caps and Double plug-in program 3.3.3 Built-in Server -- Length (20) Test host utilities 4 Creating a Plug-in 4.1 Amiga -- SAS/C Compiler (21) Makefile examples 4.2 Amiga -- Manx Compiler (22) Makefile examples 4.3 Microsoft's Windows (23) Makefile examples 4.4 SGI Unix (24) Makefile examples 4.5 Linking with LightWave (25) Config file examples (26) Config file examples 1 Plug-In Interface There are two parts to the system-generic plug-in interface: the host side and the server side. The host is the application program which wants to load external code modules to perform some generic type of operation. Servers are imported routines (which can be loaded plug-ins or internal built-ins) which implement a specific instance of a generic type of service. The host interface provides facilities to create server classes, register plug-in modules, and perform lazy loading and activation of registered servers. There is a fairly elaborate name and type mapping scheme which allows a great deal of flexibility in how modules are used, but which still provides a fairly simple interface for those who do not need the full facility. The server interface provides an easy method to write programs that will operate as plug-ins. Different classes of plug-in services will require different host interfaces, but the loading and initialization part of the server interface is standard and works with the host portion of the system. 1.1 Server Identification The plug-in interface is designed to allow the host to have any number of servers loaded to perform as many different functions as the host wants to define. The servers in the system are referenced by a combination of class and name. A Server Class is a string which determines the type of service which the server can perform. This might be strings like "TEXTURE" or "FileRequester". Many servers can have the same class, and all servers of the same class have the same host interface. A Server Name is a string which refers to a specific server within a given class. This might be something like "FractalNoise3D" or "Default". The name must be unique among the servers of the same class. The names for class and server identification should be byte strings containing characters only the the ASCII range 33-127. By convention these string contain no spaces and no characters outside 7-bit ASCII. Case is significant in distinguishing different classes and servers within classes. 1.2 Server Activation Function Every server has a single `activation' function. This is the function which the host calls to access the service provided by the server. For some servers this one function will perform the whole action and for others this will only be a prelude to a sequence of actions. Servers which must remain loaded after they return from their activation function must be locked by the host while there are actions pending or they may be unloaded. 1.3 The Global Function The activation function for every server is called with a `global' function pointer which provides access to the internal global state of the host system. The server calls the function with a string identifing the global data requested and a flag for how it will be used. The host can service this request, or the request can be passed on to global plug-in servers. 1.4 Plug-in and Built-in Servers Servers can be either plug-in or built-in. A plug-in server is implemented as a file containing code that can be loaded and unloaded as needed. A built-in server is implemented as a callback within the host itself. Having both allows the host to provide a standard set of servers which it handles the same way it handles plug-in servers, without having to unbundle their functionality in a way that can be replaced or used by other programs. 2 Server Interface A plug-in server is written like any ordinary C program, but instead of a single "main()" entry point, a server has a different primary entry point and several possible additional entry points. The server is linked with initializaion code (different from the normal shell or Workbench init) which places these interfaces where the host can access them. There are two main types of plug-in modules: those providing a single server and those providing multiple servers. It is simple to have one server per module, but it can be more efficient and useful to define many servers with a single code file. All servers require an activation function, and all plug-ins have the option of providing initialization and cleanup functions. The header for server types is `splug.h'. 2.1 Plug-in Initialization and Cleanup In both the single and multiple versions of the plug-in module, there are optional entry points which allow the module to initialize itself when it is first loaded and to clean itself up before being unloaded. If the plug-in code does not contain functions with these names, no attempt will be made to call them. The Startup function, if present, will be called when the plug-in is first loaded into the host system. The return value is global data for the server which is passed to the Activate and Shutdown entry points as ` serverData'. A zero return value (null pointer) indicates failure, so even a plug-in with no data should return something. (1) Startup usage void * Startup (void) If provided, the server's Shutdown function is called just before the server module is unloaded from the host. Any allocated server data should be freed at this point. Note that even though it is an error, this function may be called even when the server is locked, so correct cleanup should be done in this case as well. (2) Shutdown usage void Shutdown ( void *serverData) 2.2 Activation Function All servers have a single activation function which is the entry point for the host to get access to the service provided by the server. The activation function gets passed the version number for the service implementation, the `global' pointer to access global host data, class-specific `local' data, and private data maintained by the plug-in. The version number is application-defined, but typically it represents the revision of the interface that the host expects the server to use. Typically a server will not attempt to operate if the version number is greater than it expects. The ` serverData' is returned by the Startup entry point in a plug-in. The global function can be called to get global data from the host enviroment, and the contents of the `local' pointer are defined by the type of service. (3) Activation function args long version, GlobalFunc *global, void *local, void *serverData The activation function returns an error code if the attempt to call failed because of some clash between the server and the host environment. If the server was able to process the request, even it failed to complete it, it should return AFUNC_OK. If the version number is not a value which the server can explicitly handle it should return AFUNC_BADVERSION. If there is some global data the server cannot get which it requires it should return AFUNC_BADGLOBAL. Severe problems with the contents of the local data, such as some necessary pointer in the local data being null, may be reported by returning AFUNC_BADLOCAL. Any other errors from the server (running out of memory, bad filenames, user aborts, etc.) must be provided for by the specific plug-in protocol. (4) ActivateFunc type typedef int ActivateFunc (); #define AFUNC_OK 0 #define AFUNC_BADVERSION 1 #define AFUNC_BADGLOBAL 2 #define AFUNC_BADLOCAL 3 2.3 Global Function The global function passed by the host to the server is a special function which returns the pointer to some global data given by a string ID. These data blocks will often contain function pointers, but can be anything. (5) GlobalFunc types typedef void * GlobalFunc (const char *, int); . . . When a server calls the global function, it passes a string which identifies the global data required and a code for the way the data will be used. If the data pointer is not available, null is returned. The ID's that will be recognized depends on the host, on the available global plug-ins and perhaps on the server class. The use code depends on how the result of the call will be used. If the returned pointer will only be used for the course of the activation function itself, the TRANSIENT code should be used. If the data will be used after the activation function returns, such as in a server that requires locking, the ACQUIRE code should be used. In this case there must be a matching RELEASE call made when the data pointer is no longer required. RELEASE calls need only be made for ACQUIRE calls which returned a non-null pointer. The return value from a release mode global data call is undefined. (6) GlobalFunc types . . . #define GFUSE_TRANSIENT 0 #define GFUSE_ACQUIRE 1 #define GFUSE_RELEASE 2 2.4 The Global Server Class The server class given by the name "Global" is special in that it allows multiple plug-in servers to share common data or routines. In fact, the members of the Gobal class are extensions to the set of ID strings that can be passed to the "global" function. When a server calls the global function with an ID string, the host can service the request itself or has the option of pass unrecognized ID's to Global class servers of the same name. For example, if the ID is "Mambo Functions," the host may recognize this itself and return a pointer value. If it does not recognize it, it may attempt to activate a server of class "Global" with name "Mambo Functions." If such a server exists, it may be locked or unlocked, depending on the use type of the global request, and it will be called to get the value of the global pointer for the orignal requester. The activation function of a Global server is called with a GlobalService structure which will be initialized with the ID string for the request. The server must fill in the data pointer with a value which will be returned to the client, which may be null if the server wishes to deny the request. The string is passed as data so that the same activation function may be used for multiple servers. (7) Global activation data typedef struct st_GlobalService { const char *id; void *data; } GlobalService; 2.5 External Function Entry Points Functions in the plug-in get called directly by the host, and this is a funky thing in some systems since they are different environments. The XCALL_ and XCALL_INIT macros take care of everything for all different systems and compilers, so these can be used to make multi-platform servers from a single source code. XCALL_ is used on the return type, e.g. XCALL_(int) for an external entry point returning an int. XCALL_INIT is used as the first statement of the function. Both must be used for full compatibility, but XCALL_INIT is only non-null for Manx small-code modules. (8) XCALL Definitions The activation function as well as any function pointers returned from the activation function need the XCALL treatment. Startup and Shutdown do not. 2.6 Single-Service Plug-ins A single-service plug-in is a C program with an entry point for the activation callback and global symbols for the class and name of the server. There are also optional entry points for initialization and cleanup. This plug-in must contain a global character string with the name `ServerClass'. This string defines the class of this server and the server will not be loaded if this string does not match the service type string requested by the host. It must also contain a global character string called `ServerName' which holds the name for this specific server. The activation function must be called Activate, which takes the arguments as described above. (9) Activate usage XCALL_(int) Activate () 2.7 Multiple-Service Plug-ins A multiple-service plug-in is a C program which defines multiple servers through a standard set of global symbols. In particular, a multiple server module must contain a global array with the name `ServerDesc' composed of elements of the ServerRecord type. The last record in the array must have a null class name pointer. (10) ServerRecord type typedef struct st_ServerRecord { const char *class; const char *name; ActivateFunc *activate; } ServerRecord; The plug-in module may also have Startup and Shutdown entry points, and all the activate functions in the plug-in will get the same serverData as returned from the Startup function. The assumption is that the servers all share a module for some logical reason, so the sharing of global data is not unreasonable. 3 Example Plug-in Service This describes a hypothetical server class and creates some samples of plug-in modules using it. This serves as a testbed for third parties to create test plug-ins, so it should have some general capability. 3.1 String Transform Class This server class will perform manipulations on character strings, like reverse them, capitalize them, etc. This class will be "StringXfrm". A new server class is completely defined by the semantics of the activation function for the class. The activation function takes a pointer argument from the host, `local' which is a reference to data for the particular service the host needs performed. It also gets a `global' function pointer which will return global data as needed by the server. The `local' pointer will point to a StringLocal structure which holds the data for the current operation. This is a null-terminated string and the length of the string buffer, plus a temporary scratch buffer and its length. The server will overwrite `buf' with the result, and will set the `overflow' status flag if the buffers were too short. (11) Test types typedef struct st_StringLocal { char *buf; char *tmpBuf; int len, tmpLen; int overflow; } StringLocal; . . . For the string transform class of server, the global function can return a `progress' function which can be called by the server to give the user feedback about its progress. This is returned using a string ID of "Progress Function." (12) Test types . . . typedef void StringProgress (void); We'll stick these definitions into the `t_plug.h' header file for test modules to use. 3.2 String Transform Functions The functions to do string transformations are all the same. They all get the same arguments as defined by the format of the activation function. The local pointer is specific to the string transform class. There is no ` serverData' for any of the transforms since there is no Startup function. (13) String transform arguments long version, GlobalFunc *global, StringLocal *local, void *serverData String activation functions may return with an error code if the version number is wrong or if the global progress function is not available. 3.2.1 Length Operation The length operation gets the length of the string and prints that as a number into the string buffer. (14) Length function body { if (version != 1) return AFUNC_BADVERSION; if (local->len < 10) local->overflow = 1; else sprintf (local->buf, "%ld", strlen (local->buf)); return AFUNC_OK; } 3.2.2 Reverse Operation The reverse operation copies the characters from the main buffer to the temp buffer in reverse order and then copies them back. This could use a swap operation to reverse them in place, but this method demonstrates using the temp buffer and returning an overflow if the temp buffer is too small. This also calls the progress function as it swaps. (15) Reverse function body { StringProgress *progress; int len, i; if (version != 1) return AFUNC_BADVERSION; progress = (*global) ("Progress Function", GFUSE_TRANSIENT); if (!progress) return AFUNC_BADGLOBAL; len = strlen (local->buf); if (local->tmpLen <= len) { local->overflow = 1; return AFUNC_OK; } for (i = 0; i < len; i++) { local->tmpBuf[i] = local->buf[len - i - 1]; (*progress) (); } local->tmpBuf[len] = 0; strcpy (local->buf, local->tmpBuf); return AFUNC_OK; } 3.2.3 Capitalization Operation This just passes through the string and converts each letter to uppercase, calling the progress function as it goes. (16) Capitalize function body { StringProgress *progress; char *c; if (version != 1) return AFUNC_BADVERSION; progress = (*global) ("Progress Function", GFUSE_TRANSIENT); if (!progress) return AFUNC_BADGLOBAL; for (c = local->buf; *c; c++) { (*progress) (); if (*c >= 'a' && *c <= 'z') *c = *c - 'a' + 'A'; } return AFUNC_OK; } 3.2.4 Double Operation This doubles each character in the string by copying the buffer to the temp buffer and moving twice as many characters back into the buffer from there. This will fail if the buffers are not big enough. (17) Double function body { StringProgress *progress; int len, i; if (version != 1) return AFUNC_BADVERSION; progress = (*global) ("Progress Function", GFUSE_TRANSIENT); if (!progress) return AFUNC_BADGLOBAL; len = strlen (local->buf); if (local->tmpLen < len || local->len - 1 < len * 2) { local->overflow = 1; return AFUNC_OK; } strcpy (local->tmpBuf, local->buf); for (i = 0; i < len; i++) { local->buf[i * 2] = local->tmpBuf[i]; local->buf[i * 2 + 1] = local->tmpBuf[i]; (*progress) (); } local->buf[len * 2] = 0; return AFUNC_OK; } 3.3 Implementing Servers A plug-in module is really a wrapper around the activation function, and can be implemented several ways. They can be single-service plug-ins, multiple-service plug-ins, or built-in. This test includes one of each. 3.3.1 Single-Service Plug-in -- Reverse The reverse operation is implemented as a single-service plug-in, so there is one global class and server name. The activation function is called `Activate' (which it must be). The C program module itself includes the headers for the test system and server-side plug-ins. The source file is `tp_rev.c'. (18) Test Reverse plug-in program #include #include "t_plug.h" #include char ServerClass[] = "StringXfrm"; char ServerName[] = "REVERSE"; XCALL_(int) Activate () { XCALL_INIT; } 3.3.2 Multiple-Service Plug-in -- Caps & Double The capitalize and double operations are implemented as one multiple-service plug-in with two servers. The activation function entry points can have any name and are not exported symbols. They are associated with their server name in the exported array of ServerRecords which has the required name `ServerDesc'. The source file for this is `tp_cpdb.c'. (19) Test Caps and Double plug-in program #include #include "t_plug.h" #include static XCALL_(int) Capitalize () { XCALL_INIT; } static XCALL_(int) Double () { XCALL_INIT; } const char class[] = "StringXfrm"; ServerRecord ServerDesc[] = { { class, "CAPS", Capitalize }, { class, "DOUBLE", Double }, { NULL } }; 3.3.3 Built-in Server -- Length The length operation will be implemented as a built-in. As result, all we need is a local function entry point of any name in the host program. (20) Test host utilities static int ActLength () { } . . . 4 Creating a Plug-in Methods for creating plug-ins have been developed for each of the target platforms. Versions of a plug-in can be created for the different systems from a single source code with different linking instructions. Each case that follows includes an implicit makefile rule to create a ".p" plug-in module from an object file. The macro SLIB stands for the directory where the startup code and server libraries are located. SINC is the include directory and OTHER_LIBS would be any other libraries need by the module. The final section shows how to add your plug-in to the LightWave host. 4.1 Amiga -- SAS/C Compiler Linking under SAS/C on the Amiga requires replacing the normal startup code with plug-in startup code, `serv_s.o'. This can be done by using the "startup" option when using "sc link" or by placing `serv_s.o' first in the "FROM" list when using slink. Modules must also be linked with the server library. Object modules should be built without stack checking. (21) Makefile examples .o.p: sc link $(CFLAGS) startup=$(SLIB)serv_s.o $*.o\ $(SLIB)server.lib $(OTHER_LIBS) pname=$@ . . . 4.2 Amiga -- Manx Compiler Linking under the Manx compiler on the Amiga requires using the plug-in startup code `serv_m.o', which must be placed first in the list of objects passed to ln. The linker will warn about ".begin" and "_geta4" overriding library symbols, which is correct behavior in this case. They should also be linked with the "server_m" library to get the Manx server library. (22) Makefile examples . . . .o.p: ln -o $@ $(SLIB)serv_m.o $*.o -lserver_m $(OTHER_LIBS) . . . 4.3 Microsoft's Windows Plug-in modules under Windows are just DLLs created by linking with `serv_w.obj' and `server.lib'. There is no need to create a ".lib" or ".exp" file for the DLL, but the ".def" file should contain an export statement for the function `_InitServer'. A usable default def file is provided as "serv.def" in the main include directory. There is no DLL entry point function. (23) Makefile examples . . . .obj.p: link32 -dll -out:$@ -def:$(SINC)serv.def $*.obj\ $(SLIB)serv_w.obj server.lib $(OTHER_LIBS) . . . 4.4 SGI Unix Plug-in modules under IRIX are shared object modules linked with `serv_u.o' and `libserver.lib'. The DSO should export the "_mod_descrip" symbol and use "serv_u" as startup code. (24) Makefile examples . . . .o.p: ld -shared -exported_symbol _mod_descrip -L$(SLIB)\ $(SLIB)serv_u.o $*.o -o $@ -lserver $(OTHER_LIBS) 4.5 Linking with LightWave LightWave and Modeler read the names of servers from their startup configuration files. This method is much faster than scanning a directory path and allows for some user customization of plug-in names (such as national localization). It does require that the config file be accurate, since the host will blindly attempt to use servers that may not exist. This is non-fatal but may be disconcerting to the user. For Modeler, for example, the config file on the Amiga is "MOD-config"; on the SGI is ".lwmrc" and on Windows is "LWM.CFG". This file can contain any number of lines of the following form: (25) Config file examples Plugin . . . Each line describes a single server given by Class and Name. The module is the plug-in file containing the server and the user name is the string to display on the interface for describing the server's function. Class, name and module are delimited by spaces, and the user name is the rest of the line. Here are some examples (lines wrap for readability -- each statement has to be a single line). (26) Config file examples . . . Plugin CommandSequence Demo_AllBGLayers layerset.p Include Background Plugin CommandSequence Demo_NextEmptyLayer layerset.p Next Empty Plugin MeshDataEdit Demo_MakeSpikey z:lw/plugin/spikey.p Spikey Subdivide Plugin ImageLoader PDQ_Targa pdq/targa.lwp Truevision Targa Image ==================================== LightWave Images -- Stuart Ferguson 12/5/94 1 Introduction (1) Public declarations (2) Public foreward definitions 2 Image I/O Server Interface 2.1 Image Loaders (3) Public types 2.2 Image Savers (4) Public types 2.3 Result Value (5) Public declarations 2.4 Image Transfer Protocols 2.4.1 Color Protocol (6) Public types 2.4.2 Index Protocol (7) Public types 2.4.3 Generic Protocol (8) Public types (9) Public foreward definitions 2.4.4 Error Handling 2.4.5 Misc Types (10) Public declarations (11) Public declarations 2.5 Monitor Objects (12) Monitor types (13) Monitor declarations 3 Test Server 3.1 Targa Reader (14) Targa types (15) Targa functions (16) Targa utilities (17) Read targa data (18) Read targa lines (19) Read uncompressed targa line (20) Read compressed targa line (21) Read a targa pixel element into `bgra' (22) Store `bgra' pixel to line buffers 3.2 Targa Saver (23) Targa types (24) Targa functions (25) Targa utilities (26) Targa utilities (27) Targa utilities 3.3 Plugin Module (28) Targa Image server 1 Introduction This module provides interfaces for dealing with image types commonly employed by LightWave users. This allows the loading and saving of large, deep images in an expandable set of formats, and for accessing the data in a uniform manner regardless of the underlying data format. This interface is designed with plug-in image loaders and savers in mind and it provides some built-in IFF format support. Image types are given by the following values. RGB24 is an image with eight bits each of red, green and blue data for each pixel. GREY8 is an image with eight bits of greyscale value at each pixel. INDEX8 is an image with up to eight bits of color index at each pixel, mapped through a 24 bit color table. (1) Public declarations #define IMG_RGB24 0 #define IMG_GREY8 1 #define IMG_INDEX8 2 . . . Image color component, grey or index values are all unsigned chars scaled from 0 to 255. (2) Public foreward definitions typedef unsigned char ImageValue; . . . 2 Image I/O Server Interface The image input and output interfaces are designed to be extended with plug-in loaders and savers. As result, each interface really only defines the local data structure for the activation function. 2.1 Image Loaders Image loaders are servers that are called sequentially until one is able to load the image file. An application will normally have a standard format in which images are saved, so that will normally be tried first after which other loaders may be tried in any order the host can determine. If loaders are just scanned in the host plug-in database they will be called in something like alphabetical order. The activation call for a loader gets passed a pointer to a filename as well as callbacks for image data transfer. If the loader cannot open the file it sets the `result' field to IPSTAT_BADFILE and returns. If it does not recognize the file format, it sets the result to IPSTAT_NOREC. If it can load the image, it calls the `begin' callback with type of image protocol it would like. The loader then sends the data from the file to the host through the protocol and calls the `done' callback when complete to allow the source to dispose of the protocol. These callbacks are called with the `priv_data' pointer as the first field. (3) Public types typedef struct st_ImLoaderLocal { void *priv_data; int result; const char *filename; Monitor *monitor; ImageProtocolID (*begin) (void *, int type); void (*done) (void *, ImageProtocolID); } ImLoaderLocal; . . . 2.2 Image Savers Image savers are servers of "ImageSaver" class that write an image out to a file in a single specific format. The save format is typically chosen directly by the user with an interface showing the user names for the servers, so no scanning or ordering is required. The activation call for savers gets a filename, a requested protocol type, and a callback for the host to output its image data to the saver protocol. The flag in the `sendData' callback can contain the IMGF_ALPHA bit if the saver can store alpha data and IMGF_REVERSE bit if the saver wants the data sent bottom to top rather than top to bottom. The saver should create a protocol and set flags most appropriate for the destination file format. The `sendData' callback will return a non-zero error code if anything failed on the sending end or if the destination reports an error. (4) Public types . . . typedef struct st_ImSaverLocal { void *priv_data; int result; int type; const char *filename; Monitor *monitor; int (*sendData) (void *, ImageProtocolID, int); } ImSaverLocal; . . . 2.3 Result Value The result value indicates the status of the loader or saver upon completion. If the load or save was sucessful, the value should be IPSTAT_OK. If a loader fails to recognize a file as something it can load it should set the result to IPSTAT_NOREC. If the server could not open the file it should return IPSTAT_BADFILE. Any other error is just a generic failure of the loader or saver and so should set the result to IPSTAT_FAILED. Other failure modes might be possible if required in the future. (5) Public declarations . . . #define IPSTAT_OK 0 #define IPSTAT_NOREC 1 #define IPSTAT_BADFILE 2 #define IPSTAT_ABORT 3 #define IPSTAT_FAILED 99 . . . 2.4 Image Transfer Protocols Images are passed from source to destination using an image protocol. Typically, the source will select the protocol type and the destination will create a protocol of that type. The source will then send the image data to the source by calling callbacks in the protocol. Both ends are then given an opportunity to clean up. This is called a pusher protocol since the source "pushes" the data at the destination rather than the destination pulling it. There are two protocols for the three types of images: color and index protocols. The protocol `type' can have any of the same values as image type and determines the callbacks in the protocol and what they do. Protocols contain a private data pointer which should be passed as the first argument to all the callbacks. 2.4.1 Color Protocol The color protocol is used for the RGB and grey valued images (RGB24 and GREY8 types). The source starts the output by calling the `setSize' function with the width and height of the image and flags. The flags can contain the IMGF_ALPHA bit to indicate that the source data contains an alpha channel. The source then sends the data by calling the `sendLine' function with each image row number and a pointer to a line of image data and a line of alpha data, if any was indicated. For greyscale images, the image line consists of one image value per column in the image (G1 G2 ... Gw). For RGB images, this line data consists of three image values per column of the image in RGB order (R1 G1 B1 R2 G2 B2 ... Rw Gw Bw). The alpha data is in greyscale format. (6) Public types . . . typedef struct st_ColorProtocol { int type; void *priv_data; void (*setSize) (void *, int, int, int); int (*sendLine) (void *, int, const ImageValue *, const ImageValue *); int (*done) (void *, int); } ColorProtocol; . . . 2.4.2 Index Protocol Colormap index images use the index protocol. The source must first call `setSize' and `numColors' with image size, flags and number of entries in the colormap. The source must then set the colormap by calling the `setMap' callback for each entry in the colormap. Any entry which is not set is left undefined. The data in the image is then filled in using the `sendLine' function just like the greyscale case except that the image values are not grey values but colormap indices. Alpha values are in greyscale data format. (7) Public types . . . typedef struct st_IndexProtocol { int type; void *priv_data; void (*setSize) (void *, int, int, int); void (*numColors) (void *, int); void (*setMap) (void *, int, ImageValue[3]); int (*sendLine) (void *, int, const ImageValue *, const ImageValue *); int (*done) (void *, int); } IndexProtocol; . . . 2.4.3 Generic Protocol The generic protocol is either of these possibilities plus the type field for easy type identifcation. (8) Public types . . . typedef union un_ImageProtocol { int type; ColorProtocol color; IndexProtocol index; } ImageProtocol; (9) Public foreward definitions . . . typedef union un_ImageProtocol *ImageProtocolID; 2.4.4 Error Handling There are two specific mechanisms for dealing with errors that occur while using image protocols. The destination can return error codes from the `sendLine' and `done' callbacks, and the source can pass an error code to the destination's `done' callback. If an error occurs in the source of a protocol, such as a failure partway though reading a file, the source can stop calling `sendLine' prematurely. This will often trigger an error in the destination since it will have been keeping track of the amount of data sent. The source should then also pass a non-zero error code to the `done' callback which will signal an error to the destination. If an error occurs in the destination of a protocol, such as a failure partway through saving an image, the destination should start to return a non-zero error code from `sendLine.' A well-written source will stop sending data when this happens, but the destination should be prepared to continue to get lines of data and to continue to return an error code. A failed destination should also return a non-zero error code from the `done' callback. 2.4.5 Misc Types Flags to be passed to `setSize' and `sendData' callbacks. (10) Public declarations . . . #define IMGF_ALPHA 1 #define IMGF_REVERSE 2 . . . There are also some protocol macros defined to get the whole calling interface right. (11) Public declarations . . . #define IP_SETSIZE(p,w,h,f) (*(p)->setSize) ((p)->priv_data,w,h,f) #define IP_NUMCOLORS(p,n) (*(p)->numColors) ((p)->priv_data,n) #define IP_SETMAP(p,i,val) (*(p)->setMap) ((p)->priv_data,i,val) #define IP_SENDLINE(p,ln,d,a) (*(p)->sendLine) ((p)->priv_data,ln,d,a) #define IP_DONE(p,err) (*(p)->done) ((p)->priv_data,err) 2.5 Monitor Objects Monitors are simple data structures defining an interface which the server can use to give feedback to the host on its progress in performing some task. They are introduced here for file loading and saving feedback, but are used in a more general context so they are a good thing to be familiar with. A Monitor consists of some generic data and three functions: init, step and done. The `init' function is called first with the number of steps in the process to be monitored, which is computed and passed up from the server being monitored. As the task is processed, the `step' function is called with the number of steps just completed (often one). These step increments should eventually add up to the total number and then the `done' function is called, but `done' may be called early if there was a problem or the process was aborted. The `step' function will return one if the user requested an abort and zero otherwise. (12) Monitor types typedef struct st_Monitor { void *data; void (*init) (void *, unsigned int); int (*step) (void *, unsigned int); void (*done) (void *); } Monitor; The server is masked from any errors in the monitor that may occur on the host side of the interface. If there is a problem with putting up a monitor, the functions should still return normally, since the monitor is for user feedback and is not that critical. There are some macros provided to call a monitor which will do the right thing if the monitor pointer is null, which means no monitoring is necessary. MON_INCR is used for step sizes greater than one and MON_STEP is used for step sizes exactly one. (13) Monitor declarations #define MON_INIT(mon,count) if (mon) (*mon->init) (mon->data, count) #define MON_INCR(mon,d) (mon ? (*mon->step) (mon->data, d) : 0) #define MON_STEP(mon) MON_INCR (mon, 1) #define MON_DONE(mon) if (mon) (*mon->done) (mon->data) 3 Test Server This is a very simple server designed to test an alternate image format. The single plugin will load and save Targa 32 and 24 bit formats. 3.1 Targa Reader The targa loader will recognize a targa file by reading the header into this data struct. The `type' gives the compression format and interpretation of image data, `bits' gives the pixel size and `reverse' indicates if the lines will come bottom to top. (14) Targa types typedef struct st_TargaInfo { unsigned char type, bits; short width, height; int reverse; } TargaInfo; #define CKPT_TGA_BADFILE 991 #define CKPT_TGA_NOREC 992 . . . The main reader just reads the header, and if this can be matched as a targa image it reads the body. Errors will be captured by the exception context and will set the result code. (15) Targa functions int TargaLoader ( long version, GlobalFunc *global, ImLoaderLocal *local, void *servData) { ReadStrmID strm; TargaInfo tga; int fail; if (version != 1) return AFUNC_BADVERSION; if (!CkptCapture (fail)) { if (fail == CKPT_ABORT) local->result = IPSTAT_ABORT; else if (fail == CKPT_TGA_BADFILE) local->result = IPSTAT_BADFILE; else if (fail == CKPT_TGA_NOREC) local->result = IPSTAT_NOREC; else local->result = IPSTAT_FAILED; return AFUNC_OK; } strm = StrmReadOpen (local->filename, NULL); if (!strm) CkptRecover (CKPT_TGA_BADFILE); ARM1 (StrmReadClose, strm); if (!ReadTargaHeader (strm, &tga)) CkptRecover (CKPT_TGA_NOREC); StrmReadClose (strm); CkptEnd (); local->result = IPSTAT_OK; return AFUNC_OK; } . . . The reader will check just a very few things in the header before it decides it can load the file. This might be a problem since targa files are much less self-identifying than others. Any values that are out of range cause this to return 0, indicating failure to recognize. If it returns 1, the info has been read. (16) Targa utilities static int ReadTargaHeader ( ReadStrmID strm, TargaInfo *tga) { unsigned char byte, idLen; (*strm->readBytes) (strm, &idLen, 1); (*strm->readBytes) (strm, &byte, 1); if (byte) return 0; (*strm->readBytes) (strm, &tga->type, 1); if (tga->type != 2 && tga->type != 10) return 0; (*strm->skipBytes) (strm, 9); (*strm->readIWords) (strm, &tga->width, 1); (*strm->readIWords) (strm, &tga->height, 1); (*strm->readBytes) (strm, &tga->bits, 1); if (tga->bits != 24 && tga->bits != 32) return 0; (*strm->readBytes) (strm, &byte, 1); byte &= 0xF0; if (byte == 0) tga->reverse = 1; else if (byte == 0x20) tga->reverse = 0; else return 0; (*strm->skipBytes) (strm, idLen); return 1; } . . . We will always send the data in RGB24 format, since we currently only recognize targa 32 and 24 bit formats. Buffers are allocated to transfer the rgb and alpha data in the right byte-packing order. The protocol is started and recovery actions are armed in case we fail partway through. (17) Read targa data { ImageProtocolID ip; ColorProtocol *cp; ImageValue *buf, *abuf; int bufSize, i, alpha; ip = (*local->begin) (local->priv_data, IMG_RGB24); if (!ip) CkptRecover (CKPT_IO_ERROR); alpha = (tga.bits == 32); bufSize = tga.width * 4; buf = NEW_Z (bufSize); ARM_Z (buf, bufSize); abuf = (alpha ? buf + 3 * tga.width : NULL); cp = &ip->color; IP_SETSIZE (cp, tga.width, tga.height, (alpha ? IMGF_ALPHA : 0)); ARM2 (local->done, local->priv_data, cp); MON_INIT (local->monitor, tga.height); if (local->monitor) ARM1 (local->monitor->done, local->monitor->data); if (CkptBegin ()) { ARM2 ((void*)cp->done, cp->priv_data, -1); CkptEnd (); } if (IP_DONE (cp, 0)) CkptRecover (CKPT_IO_ERROR); MON_DONE (local->monitor); (*local->done) (local->priv_data, ip); FREE_Z (buf, bufSize); } Basically we just read all the lines in forward or reverse order. They may be compressed or not. (18) Read targa lines for (i = 0; i < tga.height; i++) { int ln, x; unsigned char bgra[4]; ImageValue *rgbBuf, *alphaBuf; rgbBuf = buf; alphaBuf = abuf; if (tga.type == 2) { } else { } ln = (tga.reverse ? tga.height - i - 1: i); if (IP_SENDLINE (cp, ln, buf, abuf)) break; if (MON_STEP (local->monitor)) CkptRecover (CKPT_ABORT); } Uncompressed lines of data are just `width' pixels which we read sequentially. (19) Read uncompressed targa line for (x = 0; x < tga.width; x++) { } A compressed line is enough pixels in literals and runs to fill a scanline. If the scanline is not exactly filled, this is an error. (20) Read compressed targa line x = 0; while (x < tga.width) { unsigned char test; int count, k; (*strm->readBytes) (strm, &test, 1); count = (test & 0x7F) + 1; if (test & 0x80) { for (k = 0; k < count; k++) { } } else { for (k = 0; k < count; k++) { } } x += count; } if (x != tga.width) CkptRecover (CKPT_IO_ERROR); 24 and 32 bit targa pixels are just 3 or 4 bytes in BGR(A) order. We read that into an array that will be unpacked into the format we want. (21) Read a targa pixel element into `bgra' (*strm->readBytes) (strm, bgra, (alpha ? 4 : 3)); Once we have read a pixel we can store it to the accumulating output row by sticking the rgb and optional alpha into their buffers. (22) Store `bgra' pixel to line buffers *rgbBuf++ = bgra[2]; *rgbBuf++ = bgra[1]; *rgbBuf++ = bgra[0]; if (alpha) *alphaBuf++ = bgra[3]; 3.2 Targa Saver (23) Targa types . . . typedef struct st_TargaSave { WriteStrmID strm; Monitor *mon; int width, height; int alpha, result; } TargaSave; The targa saver sets up a protocol of the RGB24 type and requests a send from the source. Since the protocol callbacks have to return result codes, the ckpt mechanism is more of a hinderance here. (24) Targa functions . . . int TargaSaver ( long version, GlobalFunc *global, ImSaverLocal *local, void *servData) { ImageProtocol prot; TargaSave tga; if (version != 1) return AFUNC_BADVERSION; if (local->type != IMG_RGB24) { local->result = IPSTAT_FAILED; return AFUNC_OK; } tga.strm = StrmWriteOpen (local->filename); if (!tga.strm) { local->result = IPSTAT_BADFILE; return AFUNC_OK; } tga.result = IPSTAT_OK; tga.mon = local->monitor; prot.type = IMG_RGB24; prot.color.priv_data = &tga; prot.color.setSize = Targa_SetSize; prot.color.sendLine = Targa_SendLine; prot.color.done = Targa_Done; (*local->sendData) (local->priv_data, &prot, IMGF_ALPHA); StrmWriteClose (tga.strm); local->result = tga.result; return AFUNC_OK; } The set size callback will just record the size and alpha status in the save info and write the header. The header is mostly zero expcpt for a few bytes with special values and the size as reversed byte order words. (25) Targa utilities . . . XCALL_(static void) Targa_SetSize ( TargaSave *tga, int w, int h, int flags) { unsigned char hdr[12]; short size[2]; int fail; if (!CkptCapture (fail)) { tga->result = IPSTAT_FAILED; return; } tga->width = w; tga->height = h; tga->alpha = ((flags & IMGF_ALPHA) != 0); memset (hdr, 0, 12); hdr[2] = 2; (*tga->strm->writeBytes) (tga->strm, hdr, 12); size[0] = w; size[1] = h; (*tga->strm->writeIWords) (tga->strm, size, 2); hdr[0] = (tga->alpha ? 32 : 24); hdr[1] = 0x20; (*tga->strm->writeBytes) (tga->strm, hdr, 2); if (tga->mon) MON_INIT (tga->mon, tga->height); CkptEnd (); } . . . Writing a line is really easy. The pixel loop just unwraps the rgb and optional alpha data into targa pixel format and writes it. Write errors will return an error code, but nothing else. (26) Targa utilities . . . static int Targa_SendLine ( TargaSave *tga, int line, const ImageValue *data, const ImageValue *adata) { unsigned char bgra[4]; int i, plen; int fail; if (tga->result != IPSTAT_OK) return -1; if (!CkptCapture (fail)) { if (fail == CKPT_ABORT) tga->result = IPSTAT_ABORT; else tga->result = IPSTAT_FAILED; return -1; } plen = (tga->alpha ? 4 : 3); for (i = 0; i < tga->width; i++) { bgra[2] = *data++; bgra[1] = *data++; bgra[0] = *data++; if (tga->alpha) bgra[3] = *adata++; (*tga->strm->writeBytes) (tga->strm, bgra, plen); } if (tga->mon && MON_STEP (tga->mon)) CkptRecover (CKPT_ABORT); CkptEnd (); return 0; } . . . The `done' callback completes the monitor transaction and returns the aggregate error status. (27) Targa utilities . . . static int Targa_Done ( TargaSave *tga, int error) { if (error) tga->result = IPSTAT_FAILED; if (tga->mon) MON_DONE (tga->mon); return (tga->result != IPSTAT_OK); } 3.3 Plugin Module (28) Targa Image server #include #include #include #include ServerRecord ServerDesc[] = { { "ImageSaver", "Targa", TargaSaver }, { "ImageLoader", "Targa", TargaLoader }, { NULL } };