Main Page Namespace List Class Hierarchy Alphabetical List Compound List Namespace Members Compound Members Related Pages

## Making an Image Import Plugin

### Overview

This tutorial will guide you to write a simple image importer for Moppi Demopaja. The image format this plugin imports is the PCX file format.

### First step

First we need to write the few functions which Demopaja loads from the DLL when the DLL is loaded. See the Creating a New Plugin Project for how to set up a new project.

### Writing a plugin class descriptor

The next step is to write a class descriptor. We will implement a class descriptor for a import plugin. At this point we also need the unique class ID for our class. This class is generated with the class ID generator which comes with the SDK.

The class descriptor of the PCX importer looks like this:

        // The Class ID
        const ClassIdC  CLASS_PCX_IMPORT( 0x74C4BF04, 0xCA444A3D );

        //  PCX importer class descriptor.
        class PCXImportDescC : public ClassDescC
        {
        public:
            PCXImportDescC();
            virtual ~PCXImportDescC();
            virtual void*           create();
            virtual int32           get_classtype() const;
            virtual SuperClassIdC   get_super_classid() const;
            virtual ClassIdC        get_classid() const;
            virtual const char*     get_name() const;
            virtual const char*     get_desc() const;
            virtual const char*     get_author_name() const;
            virtual const char*     get_copyright_message() const;
            virtual const char*     get_url() const;
            virtual const char*     get_help_filename() const;
            virtual bool            check_device_support( DeviceContextC* pContext );
            virtual uint32          get_ext_count() const;
            virtual const char*     get_ext( uint32 ui32Index ) const;
        };

        PCXImportDescC::PCXImportDescC()
        {
            // empty
        }

        PCXImportDescC::~PCXImportDescC()
        {
            // empty
        }

The create method is used to create a new instance of the PCX importer class. In the PCX importer class there is a static member method which is used to create a new instance.

        void*
        PCXImportDescC::create()
        {
            return PCXImportC::create_new();
        }

Returns the plugin class type. CLASS_TYPE_FILEIMPORT is returned for importer plugins.

        int32
        PCXImportDescC::get_classtype() const
        {
            return CLASS_TYPE_FILEIMPORT;
        }

Returns the super class ID. Since the importer class is derived from the ImageImporterI we return the superclass ID of the ImportableImageI interface.

        SuperClassIdC
        PCXImportDescC::get_super_classid() const
        {
            return SUPERCLASS_IMAGE;
        }

Returns the class ID of the importer class. This class ID has to be unique for every plugin class that is implemented.

        ClassIdC
        PCXImportDescC::get_classid() const
        {
            return CLASS_PCX_IMPORT;
        }

Returns the name of the plugin class. This name should be short but desciptive. This name is shown to the user of the Demopaja.

        const char*
        PCXImportDescC::get_name() const
        {
            return "PCX Image";
        }

Returns a short description of the plugin class.

        const char*
        PCXImportDescC::get_desc() const
        {
            return "Importer for PCX images";
        }

These methods returns some information about the author of the plugin class, such name, copyright notice and an URL where the plugin/author can be found.

        const char*
        PCXImportDescC::get_author_name() const
        {
            return "Mikko \"memon\" Mononen";
        }
        const char*
        PCXImportDescC::get_copyright_message() const
        {
            return "Copyright (c) 2000 Moppi Productions";
        }

        const char*
        PCXImportDescC::get_url() const
        {
            return "http://www.moppi.inside.org/demopaja/";
        }

Returns the file name of the help file. Help files are in HTML format. This plugin has the HTML file stored in the resources of the DLL. See Demopaja Help System for more information how to deal with help files.

    const char*
    PCXImportDescC::get_help_filename() const
    {
        return "res://pcxhelp.html";
    }

This method is used to checks if this plugin class can be created and used within the current device context. Every device the plugin class depends on should be checked. The plugin class descriptor should check the support by trying to query the devices it needs. If they can be queried, they are available.

        bool
        PCXImportDescC::check_device_support( DeviceContextC* pContext )
        {
            if( pContext->query_interface( INTERFACE_OPENGL ) )
                return true;
            return false;
        }

Returns the number of file exntensions this plugin class uses. For effect plugin classes zero should be returned, but since we are implementing an importer plugin we have to define at least one extension. Here we just return number of the extensions we have defined.

        uint32
        PCXImportDescC::get_ext_count() const
        {
            return 1;
        }

Returns an extension string at specified index. We have only one extension and it is "pcx".

        const char*
        PCXImportDescC::get_ext( uint32 ui32Index ) const
        {
            if( ui32Index == 0 )
                return "pcx";
            return 0;
        }

Now we should declare one global variable (those people who fear global variables propably can find a different way to do this) which type is the class we just defined. It is important that this variable is global (or at least static) since a pointer to it is returned to the Demopaja via the get_classdesc() function.

    PCXImportDescC      g_rPCXImportDesc;

### The Import Class

The final phase is to write the importer class itself. All importer plugin classes has to be derived from the ImporterI super class. In the Demopaja SDK there are set of interfaces which all are derived from the generic ImportableI interface. These interfaces has a set of methods which are common to all importers which are derived from it. For example image is such a generic thing that it can have an interface. The advantage of this is that all the effect plugins needs to only know how to use for example the ImportableImageI interface and they can use all the image file formats that are implemented by the importer plugins.

Here is the class definition of the PCX importer plugin class:

        class PCXImportC : public Import::ImportableImageI
        {
        public:
            static PCXImportC*              create_new();
            virtual Edit::DataBlockI*       create();
            virtual Edit::DataBlockI*       create( Edit::EditableI* pOriginal );
            virtual void                    copy( Edit::DataBlockI* pBlock );
            virtual void                    restore( Edit::EditableI* pEditable );

            virtual const char*             get_filename();

            virtual bool
            load_file( const char* szName, Import::ImportInterfaceC* pInterface );

            virtual PluginClass::ClassIdC   get_class_id();
            virtual const char*             get_class_name();

            // The importable image interface.
            virtual PajaTypes::int32        get_width();
            virtual PajaTypes::int32        get_height();
            virtual PajaTypes::int32        get_pitch();
            virtual PajaTypes::int32        get_bpp();
            virtual PajaTypes::uint8*       get_data();

            virtual void
            bind_texture( PajaSystem::DeviceInterfaceI* pInterface,
                    PajaTypes::uint32 ui32Properties );

            virtual const char*             get_info();
            virtual PluginClass::ClassIdC   get_default_effect();

            virtual PajaTypes::int32
            get_duration( PajaSystem::TimeContextC* pTimeContext );

            virtual PajaTypes::float32
            get_start_label( PajaSystem::TimeContextC* pTimeContext );

            virtual PajaTypes::float32
            get_end_label( PajaSystem::TimeContextC* pTimeContext );

            virtual PajaTypes::uint32       save( FileIO::SaveC* pSave );
            virtual PajaTypes::uint32       load( FileIO::LoadC* pLoad );

        protected:
            PCXImportC();
            PCXImportC( Edit::EditableI* pOriginal );
            virtual ~PCXImportC();

        private:

            bool    read_encoded_block( PajaTypes::uint32 ui32Size,
                            PajaTypes::uint8* pBuffer, FILE* pStream );

            PajaTypes::uint32       m_ui32TextureId;
            PajaTypes::uint8*       m_pData;
            PajaTypes::int32        m_i32Width, m_i32Height;
            PajaTypes::int32        m_i32Bpp;
            std::string         m_sFileName;
        };

Here is the implementation:

The default construtor. Set everything to the default values.

        PCXImportC::PCXImportC() :
            m_pData( 0 ),
            m_i32Width( 0 ),
            m_i32Height( 0 ),
            m_ui32TextureId( 0 )
        {
            // empty
        }

The constructor which is used when a clone of this file is made. The most important thing is to call the base class constructor, then set the data to the default values.

        PCXImportC::PCXImportC( EditableI* pOriginal ) :
            ImportableImageI( pOriginal ),
            m_pData( 0 ),
            m_i32Width( 0 ),
            m_i32Height( 0 ),
            m_ui32TextureId( 0 )
        {
            // empty
        }

The destructor. Since the data is not duplicated when a clone object is created (the importer class in this case) we don't release the data if the object is a clone. The clone can be detected by the get_original() method of the EditableI interface. If it returns a valid pointer (anything else than NULL ) then the object is a clone and no data should be released.

        PCXImportC::~PCXImportC()
        {
            // Return if this is a clone.
            if( get_original() )
                return;

            // Delete creted texture.
            if( m_ui32TextureId )
                glDeleteTextures( 1, &m_ui32TextureId );

            delete m_pData;
        }

Static member which is used to create a new instance of a plugin class. It is only a recommendation to use this method name. It could be really anything. There just have to be a way to create a new instance of the plugin class.

        PCXImportC*
        PCXImportC::create_new()
        {
            return new PCXImportC;
        }

This method is analogous to the default constructor.

        DataBlockI*
        PCXImportC::create()
        {
            return new PCXImportC;
        }

This method is analogous to the clone constructor.

        DataBlockI*
        PCXImportC::create( EditableI* pOriginal )
        {
            return new PCXImportC( pOriginal );
        }

This method is used to copy data from a specified datablock (the datablock is guaranteed to be of the same class as the class which copy() method is called). All the data should be copied (deep copy).

        void
        PCXImportC::copy( DataBlockI* pBlock )
        {
            PCXImportC* pFile = (PCXImportC*)pBlock;

            m_ui32TextureId = pFile->m_ui32TextureId;
            m_i32Width = pFile->m_i32Width;
            m_i32Height = pFile->m_i32Height;
            m_i32Bpp = pFile->m_i32Bpp;
            m_sFileName = pFile->m_sFileName;

            // Duplicate data.
            delete m_pData;
            uint32  ui32DataSize = m_i32Width * m_i32Height * (m_i32Bpp / 8);
            m_pData = new uint8[ui32DataSize];
            memcpy( m_pData, pFile->m_pData, ui32DataSize );
        }

This method is used to make shallow copy from a specified editable (the editable is guaranteed to be of the same class as the class which copy() method is called). Only the variables should be copied, no the data they point to. This method is used first to store the data to a clone object and then later on to restore the data from the clone, hence the name.

        void
        PCXImportC::restore( EditableI* pEditable )
        {
            PCXImportC* pFile = (PCXImportC*)pEditable;

            m_ui32TextureId = pFile->m_ui32TextureId;
            m_pData = pFile->m_pData;
            m_i32Width = pFile->m_i32Width;
            m_i32Height = pFile->m_i32Height;
            m_i32Bpp = pFile->m_i32Bpp;
            m_sFileName = pFile->m_sFileName;
        }

Returns the name of the file attached to this importable. This name is shown in the GUI (first the path is stripped off) and is also used to reload the file. So be sure to store the original file name.

        const char*
        PCXImportC::get_filename()
        {
            return m_sFileName.c_str();
        }

This method is used by the PCX loader. It reads a RLE encoded block from the file.

        bool
        PCXImportC::read_encoded_block( uint32 ui32Size, uint8* pBuffer, FILE* pStream )
        {
            uint32  ui32Count;
            uint32  ui32Val;
            uint8*  pDest = pBuffer;

            for( uint32 i = 0; i < ui32Size; ) {

                ui32Val = fgetc( pStream );
                ui32Count = 1;

                if( feof( pStream ) )
                    return true;

                if( (ui32Val & 0xC0) == 0xC0 ) {
                    ui32Count = ui32Val & 0x3F;
                    ui32Val = fgetc( pStream );
                    if( feof( pStream ) )
                        return false;
                }

                for( uint32 j = 0; j < ui32Count; j++ )
    pDest++ = (uint8)ui32Val;

                i += ui32Count;
            }

            return true;
        }

Loads the file. It is convenient to store the file name in this method. The import interface is not used in this importer. Most the code is PCX file format specific.

        bool
        PCXImportC::load_file( const char* szName, ImportInterfaceC* pInterface )
        {
            FILE*       pStream;
            PCXHeaderS  rHeader;

            if( (pStream = fopen( szName, "rb" )) == 0 ) {
                return false;
            }

            // Read header
            fread( &rHeader, 1, sizeof( rHeader ), pStream );

            // We only support BPP images
            if( rHeader.m_ui8BPP != 8 )
                return false;

            // Store width and height
            m_i32Width = rHeader.m_ui16BytesPerLine;
            m_i32Height = (rHeader.m_ui16YMax - rHeader.m_ui16YMin) + 1;

            if( rHeader.m_ui8NPlanes == 1 ) {
                // Read 8-bit image
                uint32  ui32Size = m_i32Width * m_i32Height;
                uint8*  pBuffer = new uint8[ui32Size];

                // Encode
                if( !read_encoded_block( ui32Size, pBuffer, pStream ) )
                    return false;

                if( rHeader.m_ui16PalInfo == 2 ) {
                    // Greyscale
                    m_pData = pBuffer;
                    m_i32Bpp = 8;
                }
                else {
                    // Read palette.
                    uint8   ui8Palette[256 * 3];
                    fseek( pStream, SEEK_END, -768 );
                    fread( ui8Palette, 256 * 3, 1, pStream );

                    m_pData = new uint8[m_i32Width * m_i32Height * 3];
                    uint8*  pDest = m_pData;
                    uint8*  pSrc = pBuffer;

                    // Convert paletted to RGB
                    for( uint32 i = 0; i < m_i32Height; i++ ) {
                        for( uint32 j = 0; j < m_i32Width; j++ ) {
    pDest++ = ui8Palette[(*pSrc) * 3 + 2];
    pDest++ = ui8Palette[(*pSrc) * 3 + 1];
    pDest++ = ui8Palette[(*pSrc) * 3 + 0];
                            pSrc++;
                        }
                    }

                    m_i32Bpp = 24;
                    delete [] pBuffer;
                }

            }
            else if( rHeader.m_ui8NPlanes == 3 ) {
                // Read 24-bit image.
                uint32  ui32Size = m_i32Width * m_i32Height * 3;
                uint8*  pBuffer = new uint8[ui32Size];

                // Encode
                if( !read_encoded_block( ui32Size, pBuffer, pStream ) )
                    return false;

                m_pData = new uint8[ui32Size];
                uint8*  pDest = m_pData;

                // Convert to interleaved RGB
                for( uint32 i = 0; i < m_i32Height; i++ ) {
                    for( uint32 j = 0; j < m_i32Width; j++ ) {
    pDest++ =
                        pBuffer[i * m_i32Width * 3 + j];
    pDest++ =
                        pBuffer[i * m_i32Width * 3 + j + m_i32Width];
    pDest++ =
                        pBuffer[i * m_i32Width * 3 + j + m_i32Width * 2];
                    }
                }

                delete [] pBuffer;

                m_i32Bpp = 24;
            }

            fclose( pStream );

            // Store file name.
            m_sFileName = szName;

            return true;
        }

Returns the class ID. This method should return the same information as the method of same name in the class descriptor.

        ClassIdC
        PCXImportC::get_class_id()
        {
            return CLASS_PCX_IMPORT;
        }

Returns the name of the class. This method should return the same information as the method of same name in the class descriptor.

        const char*
        PCXImportC::get_class_name()
        {
            return "PCX Image";
        }

Returns the width of the image in pixels. This method is from the ImportableImageI interface.

        int32
        PCXImportC::get_width()
        {
            return m_i32Width;
        }

Returns the height of the image in pixels. This method is from the ImportableImageI interface.

        int32
        PCXImportC::get_height()
        {
            return m_i32Height;
        }

Returns the pitch of the image in bytes. In some cases it is necessary to store the image in the memory so that the length of each row in the data is not same as the width of the image. This method is from the ImportableImageI interface.

        int32
        PCXImportC::get_pitch()
        {
            return m_i32Width * m_i32BPP / 8;
        }

Returns the Bits Per Pixel (BPP) of the image. This method is from the ImportableImageI interface.

        int32
        PCXImportC::get_bpp()
        {
            return m_i32Bpp;
        }

Returns pointer to the data of the image. This method is from the ImportableImageI interface.

        uint8*
        PCXImportC::get_data()
        {
            return m_pData;
        }

Sets the image as current texture on specified device. This method enables the texture only be stored in the graphic card once. In this case this is done via the texture objects in OpenGL. This method is from the ImportableImageI interface.

        void
        PCXImportC::bind_texture( DeviceInterfaceI* pInterface, uint32 ui32Properties )
        {
            if( !pInterface || pInterface->get_type() != INTERFACE_OPENGL )
                return;

            if( !m_ui32TextureId ) {

                glGenTextures( 1, &m_ui32TextureId );
                glBindTexture( GL_TEXTURE_2D, m_ui32TextureId );
                glTexEnvi( GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE );

                if( m_i32Bpp == 8 ) {
                    glTexImage2D( GL_TEXTURE_2D, 0, GL_ALPHA , m_i32Width,
                    m_i32Height, 0, GL_ALPHA , GL_UNSIGNED_BYTE, m_pData );
                }
                else if( m_i32Bpp == 24 ) {
                    glTexImage2D( GL_TEXTURE_2D, 0, GL_RGB, m_i32Width,
                    m_i32Height, 0, GL_RGB, GL_UNSIGNED_BYTE, m_pData );
                }
                else if( m_i32Bpp ==32 ) {
                    glTexImage2D( GL_TEXTURE_2D, 0, GL_RGBA, m_i32Width,
                    m_i32Height, 0, GL_RGBA, GL_UNSIGNED_BYTE, m_pData );
                }
            }
            else
                glBindTexture( GL_TEXTURE_2D, m_ui32TextureId );

            if( ui32Properties & IMAGE_LINEAR ) {
                glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR );
                glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR );
            }
            else {
                glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST );
                glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST );
            }

            if( ui32Properties & IMAGE_CLAMP ) {
                glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP );
                glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP );
            }
            else {
                glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT );
                glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT );
            }
        }

Returns short info about the importable. In this case the importable is an image, and the image dimension and image type is returned.

        const char*
        PCXImportC::get_info()
        {
            static char szInfo[256];
            _snprintf( szInfo, 255, "%s %d x %d x %dbit",
                m_i32Bpp == 8 ? "GREY" : "RGB", m_i32Width, m_i32Height, m_i32Bpp );
            return szInfo;
        }

Returns the default effect which is able to show this data. This method is used when a new effect is created by drag'n'drop operation.

        ClassIdC
        PCXImportC::get_default_effect()
        {
            return CLASS_SIMPLEIMAGE_EFFECT;
        }

Returns the duration of the data. Since the data in this case is a still image return -1, which indicates, that there is no duration.

        int32
        PCXImportC::get_duration( TimeContextC* pTimeContext )
        {
            return -1;
        }

Return dummy for labels. The labels are only used if the importable has animation. The start label is the first frame number, and the end label is the last frame number.

        float32
        PCXImportC::get_start_label( TimeContextC* pTimeContext )
        {
            return 0;
        }

        float32
        PCXImportC::get_end_label( TimeContextC* pTimeContext )
        {
            return 0;
        }

Save the importable to a Demopaja stream. The data is written in two chunks. The first chunk contains the name of the file and the second cintains the actual data.

        enum PCXImportChunksE {
            CHUNK_PCXIMPORT_BASE =  0x1000,
            CHUNK_PCXIMPORT_DATA =  0x2000,
        };

        const uint32    PCXIMPORT_VERSION = 1;

        uint32
        PCXImportC::save( SaveC* pSave )
        {
            uint32      ui32Error = IO_OK;
            uint8       ui8Tmp;
            std::string sStr;

            // file base
            pSave->begin_chunk( CHUNK_PCXIMPORT_BASE, PCXIMPORT_VERSION );
                sStr = m_sFileName;
                if( sStr.size() > 255 )
                        sStr.resize( 255 );
                ui32Error = pSave->write_str( sStr.c_str() );
            pSave->end_chunk();

            // file data
            pSave->begin_chunk( CHUNK_PCXIMPORT_DATA, PCXIMPORT_VERSION );
                ui32Error = pSave->write( &m_i32Width, sizeof( m_i32Width ) );
                ui32Error = pSave->write( &m_i32Height, sizeof( m_i32Height ) );
                ui8Tmp = (uint8)m_i32Bpp;
                ui32Error = pSave->write( &ui8Tmp, sizeof( ui8Tmp ) );
                ui32Error = pSave->write( m_pData,
                        m_i32Width * m_i32Height * (m_i32Bpp / 8) );
            pSave->end_chunk();

            return ui32Error;
        }

Loads the importable from a Demopaja stream.

        uint32
        PCXImportC::load( LoadC* pLoad )
        {
            uint32  ui32Error = IO_OK;
            char    szStr[256];
            uint8   ui8Tmp;

            while( (ui32Error = pLoad->open_chunk()) == IO_OK ) {

                switch( pLoad->get_chunk_id() ) {
                case CHUNK_PCXIMPORT_BASE:
                    if( pLoad->get_chunk_version() == PCXIMPORT_VERSION ) {
                        ui32Error = pLoad->read_str( szStr );
                        m_sFileName = szStr;
                    }
                    break;

                case CHUNK_PCXIMPORT_DATA:
                    if( pLoad->get_chunk_version() == PCXIMPORT_VERSION ) {
                        // delete old data if any
                        delete m_pData;
                        // load new data
                        ui32Error = pLoad->read( &m_i32Width,
                            sizeof( m_i32Width ) );
                        ui32Error = pLoad->read( &m_i32Height,
                            sizeof( m_i32Height ) );
                        ui32Error = pLoad->read( &ui8Tmp,
                            sizeof( ui8Tmp ) );
                        m_i32Bpp = ui8Tmp;
                        m_pData = new uint8[m_i32Width * m_i32Height *
                            (m_i32Bpp / 8)];
                        ui32Error = pLoad->read( m_pData,
                            m_i32Width * m_i32Height * (m_i32Bpp / 8) );
                    }
                    break;

                default:
                    assert( 0 );
                }

                pLoad->close_chunk();

                if( ui32Error != IO_OK && ui32Error != IO_END ) {
                    return ui32Error;
                }
            }

            return ui32Error;
        }

Now we have created an importer plugin which imports PCX images to Demopaja for further use. The source for this plugin can be found in the `Examples\ImagePlugin` directory. That plugin also contains a simple effect which shows the data.