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

## Making an Effect Plugin

This tutorial will guide you to write a simple effect plugin for Moppi Demopaja.

### 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 an effect 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 image effect looks like this:

    // The class ID
    const ClassIdC  CLASS_SIMPLEIMAGE_EFFECT( 0x08DF0B9D, 0xFF4B4769 );

    //  Simple image effect class descriptor.
    class ImageDescC : public ClassDescC
    {
    public:
        ImageDescC();
        virtual ~ImageDescC();
        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;
    };

    ImageDescC::ImageDescC()
    {
        // empty
    }

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

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

    void*
    ImageDescC::create()
    {
        return (void*)ImageEffectC::create_new();
    }

Returns the plugin class type. CLASS_TYPE_EFFECT is returned for all effect plugins. The system determines using this method wheter the plugin class is importer or effect.

    int32
    ImageDescC::get_classtype() const
    {
        return CLASS_TYPE_EFFECT;
    }

Returns the super class ID. All effects returns SUPERCLASS_EFFECT. It indicates that the effect is derived from EffectI class.

    SuperClassIdC
    ImageDescC::get_super_classid() const
    {
        return SUPERCLASS_EFFECT;
    }

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

    ClassIdC
    ImageDescC::get_classid() const
    {
        return CLASS_SIMPLEIMAGE_EFFECT;
    };

Returns a short description of the plugin class.

    const char*
    ImageDescC::get_desc() const
    {
        return "Simple Image effect";
    }

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*
    ImageDescC::get_name() const
    {
        return "Simple Image";
    }

    const char*
    ImageDescC::get_author_name() const
    {
        return "Mikko \"memon\" Mononen";
    }

    const char*
    ImageDescC::get_copyright_message() const
    {
        return "Copyright (c) 2000 Moppi Productions";
    }

    const char*
    ImageDescC::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*
    ImageDescC::get_help_filename() const
    {
        return "res://imagehelp.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
    ImageDescC::check_device_support( DeviceContextC* pContext )
    {
        if( pContext->query_interface( INTERFACE_OPENGL ) )
            return true;
        return false;
    }

These two methods are not used for effect plugins. These are used with importer plugin classes and they return the file extensions which the plugin could load.

    uint32
    ImageDescC::get_ext_count() const
    {
        return 0;
    }

    const char*
    ImageDescC::get_ext( uint32 ui32Index ) const
    {
        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.

    ImageDescC      g_rImageDesc;

### The Effect Class

The last thing to do is to write the effect class. The effect class will contain some gizmos, which in turn contains the parameters. Usually it would be required to implement some GizmoI derived classes where the gizmos would be stored, but since the effect we work on is so simple we use a AutoGizmoI class instead. The auto gizmo class enables the gizmo to be filled with parameters with out need to write derived gizmo class. Even the auto gizmo saves the data inside parameters it does not save the information about itself. That is, the gizmo has to be populated with the same kind of parameters before it is loaded. It is safe to use the auto gizmo in the way it is represented in this example.

This effect simple has six parameters put in two gizmos. The first gizmo "Transform" holds position, pivot, scale, and rotation parameters, the second gizmo "Attributes" holds color and image parameters.

Here is the class definition of the image effect plugin class:

    // IDs for the transformation parameters.
    enum TransformGizmoParamsE {
        ID_TRANSFORM_POS = 0,
        ID_TRANSFORM_PIVOT,
        ID_TRANSFORM_ROT,
        ID_TRANSFORM_SCALE,
        TRANSFORM_COUNT,
    };

    // IDs for the attribute parameters.
    enum AttributeGizmoParamsE {
        ID_ATTRIBUTE_COLOR = 0,
        ID_ATTRIBUTE_FILE,
        ATTRIBUTE_COUNT,
    };

    // Gizmo IDs.
    enum ImageEffectGizmosE {
        ID_GIZMO_TRANS = 0,
        ID_GIZMO_ATTRIB,
        GIZMO_COUNT,
    };

    // The Image effect class.
    class ImageEffectC : public Composition::EffectI
    {
    public:
        static ImageEffectC*            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 PajaTypes::int32        get_gizmo_count();
        virtual Composition::GizmoI*    get_gizmo( PajaTypes::int32 i32Index );

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

        virtual void                    set_default_file( Import::FileHandleC* pHandle );
        virtual Composition::ParamI*    get_default_param( PajaTypes::int32 i32Param );

        virtual void                    initialize( PajaSystem::DeviceContextC* pContext, PajaSystem::TimeContextC* pTimeContext );

        virtual void                    do_frame( PajaSystem::DeviceContextC* pContext );
        virtual void                    eval_state( PajaTypes::int32 i32Time, PajaSystem::TimeContextC* pTimeContext );
        virtual PajaTypes::BBox2C       get_bbox();

        virtual const PajaTypes::Matrix2C&  get_transform_matrix() const;

        virtual bool                    hit_test( const PajaTypes::Vector2C& rPoint );

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

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

    private:

        Composition::AutoGizmoC*    m_pTraGizmo;
        Composition::AutoGizmoC*    m_pAttGizmo;

        PajaTypes::Matrix2C m_rTM;
        PajaTypes::BBox2C   m_rBBox;
        PajaTypes::Vector2C m_rVertices[4];
        PajaTypes::ColorC   m_rFillColor;
    };

Here is the implementation:

Default constructor. Create the gizmos and the parameters and put the into the gizmos. Since we do the parameter initialisation here, the auto gizmos will be filled before the serialization methods are called.

    ImageEffectC::ImageEffectC()
    {
        // Create Transform gizmo.
        m_pTraGizmo = AutoGizmoC::create_new( this, "Transform", ID_GIZMO_TRANS );

        m_pTraGizmo->add_parameter( ParamVector2C::create_new( m_pTraGizmo, "Position", Vector2C(), ID_TRANSFORM_POS,
                            PARAM_STYLE_EDITBOX | PARAM_STYLE_ABS_POSITION, PARAM_ANIMATABLE ) );

        m_pTraGizmo->add_parameter( ParamVector2C::create_new( m_pTraGizmo, "Pivot", Vector2C(), ID_TRANSFORM_PIVOT,
                            PARAM_STYLE_EDITBOX | PARAM_STYLE_REL_POSITION | PARAM_STYLE_WORLD_SPACE, PARAM_ANIMATABLE ) );

        m_pTraGizmo->add_parameter( ParamFloatC::create_new( m_pTraGizmo, "Rotation", 0, ID_TRANSFORM_ROT,
                            PARAM_STYLE_EDITBOX | PARAM_STYLE_ANGLE, PARAM_ANIMATABLE, 0, 0, 1.0f ) );

        m_pTraGizmo->add_parameter( ParamVector2C::create_new( m_pTraGizmo, "Scale", Vector2C( 1, 1 ), ID_TRANSFORM_SCALE,
                            PARAM_STYLE_EDITBOX | PARAM_STYLE_PERCENT, PARAM_ANIMATABLE, Vector2C(), Vector2C(), 0.01f ) );

        // Create Attributes gizmo.
        m_pAttGizmo = AutoGizmoC::create_new( this, "Attributes", ID_GIZMO_ATTRIB );

        m_pAttGizmo->add_parameter( ParamColorC::create_new( m_pAttGizmo, "Fill Color", ColorC( 1, 1, 1, 1 ), ID_ATTRIBUTE_COLOR,
                                PARAM_STYLE_COLORPICKER, PARAM_ANIMATABLE ) );
        m_pAttGizmo->add_parameter( ParamFileC::create_new( m_pAttGizmo, "Fill Image", SUPERCLASS_IMAGE, NULL_CLASSID, ID_ATTRIBUTE_FILE ) );
    }

Close constructor. Initialize only.

    ImageEffectC::ImageEffectC( EditableI* pOriginal ) :
        EffectI( pOriginal ),
        m_pTraGizmo( 0 ),
        m_pAttGizmo( 0 )
    {
        // Empty. The parameters are not created in the clone constructor.
    }

The destructor. Since the data is not duplicated when a clone object is created 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.

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

        // Release gizmos.
        m_pTraGizmo->release();
        m_pAttGizmo->release();
    }

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.

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

This method is analogous to the default constructor.

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

This method is analogous to the clone constructor.

    DataBlockI*
    ImageEffectC::create( EditableI* pOriginal )
    {
        return new ImageEffectC( 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
    ImageEffectC::copy( DataBlockI* pBlock )
    {
        EffectI::copy( pBlock );

        // Make deep copy.
        ImageEffectC*   pEffect = (ImageEffectC*)pBlock;
        m_pTraGizmo->copy( pEffect->m_pTraGizmo );
        m_pAttGizmo->copy( pEffect->m_pAttGizmo );
    }

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
    ImageEffectC::restore( EditableI* pEditable )
    {
        EffectI::restore( pEditable );

        // Make shallow copy.
        ImageEffectC*   pEffect = (ImageEffectC*)pEditable;
        m_pTraGizmo = pEffect->m_pTraGizmo;
        m_pAttGizmo = pEffect->m_pAttGizmo;
    }

Returns number of gizmos inside the effect.

    int32
    ImageEffectC::get_gizmo_count()
    {
        // Return number of gizmos inside this effect.
        return GIZMO_COUNT;
    }

Returns the gizmo at specified index.

    GizmoI*
    ImageEffectC::get_gizmo( PajaTypes::int32 i32Index )
    {
        // Returns specified gizmo.
        // Since the ID's are zero based, we can use them as indices.
        switch( i32Index ) {
        case ID_GIZMO_TRANS:
            return m_pTraGizmo;
        case ID_GIZMO_ATTRIB:
            return m_pAttGizmo;
        }

        return 0;
    }

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

    ClassIdC
    ImageEffectC::get_class_id()
    {
        // Return the class ID. Should be same as in the class descriptor.
        return CLASS_SIMPLEIMAGE_EFFECT;
    }

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*
    ImageEffectC::get_class_name()
    {
        // Return the class name. Should be same as in the class descriptor.
        return "Simple Image";
    }

Sets the default file. This method is called when an effect is created by draggin an imported file to the Layer list. This method should set the file parameter which is ment to be the default for this effect. The file parameter checks that the file is in format allowed by the class filters.

    void
    ImageEffectC::set_default_file( FileHandleC* pHandle )
    {
        // Sets the default file.

        // Get the file parameter.
        ParamFileC* pParam = (ParamFileC*)m_pAttGizmo->get_parameter( ID_ATTRIBUTE_FILE );

        // Begin Undo block.
        UndoC*  pOldUndo = pParam->begin_editing( get_undo() );

        // Set the file.
        pParam->set_file( pHandle );

        // End undo block.
        pParam->end_editing( pOldUndo );
    }

Returns defautl parameters used by the user interface. These four are for transformation.

    ParamI*
    ImageEffectC::get_default_param( int32 i32Param )
    {
        // Return specified default parameter.
        if( i32Param == DEFAULT_PARAM_POSITION )
            return m_pTraGizmo->get_parameter( ID_TRANSFORM_POS );
        else if( i32Param == DEFAULT_PARAM_ROTATION )
            return m_pTraGizmo->get_parameter( ID_TRANSFORM_ROT );
        else if( i32Param == DEFAULT_PARAM_SCALE )
            return m_pTraGizmo->get_parameter( ID_TRANSFORM_SCALE );
        else if( i32Param == DEFAULT_PARAM_PIVOT )
            return m_pTraGizmo->get_parameter( ID_TRANSFORM_PIVOT );
        return 0;
    }

Initializes the effect. All the initializing which requires the information about the device or time code, should be done here. Also, if parameters need to be copied (for example the gizmo layout has changes between two version of the effect) in this method, since the file handles in file parameters are not valid when the effect is being loaded. When a new effect is created this method is called after data is passed to the effect (default parameter and name). If an effect is duplicated (for example using Copy/Paste) this method is not called. This means that, the parameters initialized here should be copied in the copy() method.

    void
    ImageEffectC::initialize( DeviceContextC* pContext, TimeContextC* pTimeContext )
    {
        // Empty, nothign to initialize.
    }

Updates the effect state at specified time. The main purpose is to generate here the data the user interface will need. The most important things are: transformation matrix, bounding box, and possible data which may be needed in hittesting.

    void
    ImageEffectC::eval_state( int32 i32Time, TimeContextC* pTimeContext )
    {
        Matrix2C    rPosMat, rRotMat, rScaleMat, rPivotMat;

        Vector2C    rScale;
        Vector2C    rPos;
        Vector2C    rPivot;
        float32     f32Rot;

        // Get parameters which affect the transformation.
        ((ParamVector2C*)m_pTraGizmo->get_parameter( ID_TRANSFORM_POS ))->get_val( i32Time, rPos );
        ((ParamVector2C*)m_pTraGizmo->get_parameter( ID_TRANSFORM_PIVOT ))->get_val( i32Time, rPivot );
        ((ParamFloatC*)m_pTraGizmo->get_parameter( ID_TRANSFORM_ROT ))->get_val( i32Time, f32Rot );
        ((ParamVector2C*)m_pTraGizmo->get_parameter( ID_TRANSFORM_SCALE ))->get_val( i32Time, rScale );

        // Calculate transformation matrix.
        rPivotMat.set_trans( rPivot );
        rPosMat.set_trans( rPos );
        rRotMat.set_rot( f32Rot / 180.0f * M_PI );
        rScaleMat.set_scale( rScale ) ;
        m_rTM = rPivotMat * rRotMat * rScaleMat * rPosMat;

        float32     f32Width = 25;
        float32     f32Height = 25;
        Vector2C    rMin, rMax;
        Vector2C    rVec;

        // Get the size from the fiel or use the defautls if no file.
        ImportableImageI*   pImp = 0;
        FileHandleC*        pHandle = ((ParamFileC*)m_pAttGizmo->get_parameter( ID_ATTRIBUTE_FILE ))->get_file();

        if( pHandle )
            pImp = (ImportableImageI*)pHandle->get_importable();

        if( pImp ) {
            f32Width = (float32)pImp->get_width() * 0.5f;
            f32Height = (float32)pImp->get_height() * 0.5f;
        }

        // Calcualte vertices of the rectangle.
        m_rVertices[0][0] = -f32Width;      // top-left
        m_rVertices[0][1] = -f32Height;

        m_rVertices[1][0] =  f32Width;      // top-right
        m_rVertices[1][1] = -f32Height;

        m_rVertices[2][0] =  f32Width;      // bottom-right
        m_rVertices[2][1] =  f32Height;

        m_rVertices[3][0] = -f32Width;      // bottom-left
        m_rVertices[3][1] =  f32Height;

        // Calculate bounding box
        for( uint32 i = 0; i < 4; i++ ) {
            rVec = m_rTM * m_rVertices[i];
            m_rVertices[i] = rVec;

            if( !i )
                rMin = rMax = rVec;
            else {
                if( rVec[0] < rMin[0] ) rMin[0] = rVec[0];
                if( rVec[1] < rMin[1] ) rMin[1] = rVec[1];
                if( rVec[0] > rMax[0] ) rMax[0] = rVec[0];
                if( rVec[1] > rMax[1] ) rMax[1] = rVec[1];
            }
        }

        // Store bounding box.
        m_rBBox[0] = rMin;
        m_rBBox[1] = rMax;

        // Get fill color.
        ((ParamColorC*)m_pAttGizmo->get_parameter( ID_ATTRIBUTE_COLOR ))->get_val( i32Time, m_rFillColor );
    }

Renders the frame.

    void
    ImageEffectC::do_frame( DeviceContextC* pContext )
    {
        // Get the OpenGL interface.
        OpenGLInterfaceC*   pInterface = (OpenGLInterfaceC*)pContext->query_interface( INTERFACE_OPENGL );

        // Return if not present.
        if( !pInterface )
            return;

        // Set orthographic projection.
        pInterface->set_ortho( m_rBBox, m_rBBox[0][0], m_rBBox[1][0], m_rBBox[1][1], m_rBBox[0][1] );

        // Get file handle and image.
        FileHandleC*        pHandle = 0;
        ImportableImageI*   pImp = 0;

        pHandle = ((ParamFileC*)m_pAttGizmo->get_parameter( ID_ATTRIBUTE_FILE ))->get_file();
        if( pHandle )
            pImp = (ImportableImageI*)pHandle->get_importable();

        if( pImp ) {
            // If there is image set it as current texture.
            pImp->bind_texture( pInterface, IMAGE_CLAMP | IMAGE_LINEAR );
            glEnable( GL_TEXTURE_2D );
        }
        else
            glDisable( GL_TEXTURE_2D );

        glDisable( GL_DEPTH_TEST );
        glEnable( GL_BLEND );
        glBlendFunc( GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA );
        glDepthMask( GL_FALSE );

        // Set color
        glColor4fv( m_rFillColor );

        // Draw rectangle.
        glBegin( GL_QUADS );

        glTexCoord2f( 0, 1 );
        glVertex2f( m_rVertices[0][0], m_rVertices[0][1] );

        glTexCoord2f( 1, 1 );
        glVertex2f( m_rVertices[1][0], m_rVertices[1][1] );

        glTexCoord2f( 1, 0 );
        glVertex2f( m_rVertices[2][0], m_rVertices[2][1] );

        glTexCoord2f( 0, 0 );
        glVertex2f( m_rVertices[3][0], m_rVertices[3][1] );

        glEnd();

        glDepthMask( GL_TRUE );
    }

Returns the bounding box of the effect. The whole effect should fit inside this box. The box is axis-aligned.

    BBox2C
    ImageEffectC::get_bbox()
    {
        // Return the bounding box.
        return m_rBBox;
    }

Returns the transformation matrix of the effect.

    const Matrix2C&
    ImageEffectC::get_transform_matrix() const
    {
        // Return the trnasformation matrix.
        return m_rTM;
    }

Tests if the specified point is inside the the effects rendering are. Simple point in polygon testing is used here. The hit testing should be as accurate as possible, but it cannot be defined hit test the bounding box. The code here is taken from the comp.graphics.algorithms newsgour FAQ.

    bool
    ImageEffectC::hit_test( const Vector2C& rPoint )
    {
        // Point in polygon test.
        // from c.g.a FAQ
        int     i, j;
        bool    bInside = false;

        for( i = 0, j = 4 - 1; i < 4; j = i++ ) {
            if( ( ((m_rVertices[i][1] <= rPoint[1]) && (rPoint[1] < m_rVertices[j][1])) ||
                ((m_rVertices[j][1] <= rPoint[1]) && (rPoint[1] < m_rVertices[i][1])) ) &&
                (rPoint[0] < (m_rVertices[j][0] - m_rVertices[i][0]) * (rPoint[1] - m_rVertices[i][1]) / (m_rVertices[j][1] - m_rVertices[i][1]) + m_rVertices[i][0]) )

                bInside = !bInside;
        }

        return bInside;
    }

Some constant for serializing the effect.

    enum ImageEffectChunksE {
        CHUNK_IMAGEEFFECT_BASE =        0x1000,
        CHUNK_IMAGEEFFECT_TRANSGIZMO =  0x2000,
        CHUNK_IMAGEEFFECT_ATTRIBGIZMO = 0x3000,
    };

    const uint32    IMAGEEFFECT_VERSION = 1;

Saves the effect to Demopaja output stream. It usually won't get any complicated than this. It is important to call the EffectI's save method too.

    uint32
    ImageEffectC::save( SaveC* pSave )
    {
        uint32  ui32Error = IO_OK;

        // EffectI base class
        pSave->begin_chunk( CHUNK_IMAGEEFFECT_BASE, IMAGEEFFECT_VERSION );
            ui32Error = EffectI::save( pSave );
        pSave->end_chunk();

        // Transform
        pSave->begin_chunk( CHUNK_IMAGEEFFECT_TRANSGIZMO, IMAGEEFFECT_VERSION );
            ui32Error = m_pTraGizmo->save( pSave );
        pSave->end_chunk();

        // Attribute
        pSave->begin_chunk( CHUNK_IMAGEEFFECT_ATTRIBGIZMO, IMAGEEFFECT_VERSION );
            ui32Error = m_pAttGizmo->save( pSave );
        pSave->end_chunk();

        return ui32Error;
    }

Loads the effect from Demopaja input stream.

    uint32
    ImageEffectC::load( LoadC* pLoad )
    {
        uint32  ui32Error = IO_OK;

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

            switch( pLoad->get_chunk_id() ) {
            case CHUNK_IMAGEEFFECT_BASE:
                // EffectI base class
                if( pLoad->get_chunk_version() == IMAGEEFFECT_VERSION )
                    ui32Error = EffectI::load( pLoad );
                break;

            case CHUNK_IMAGEEFFECT_TRANSGIZMO:
                // Transform
                if( pLoad->get_chunk_version() == IMAGEEFFECT_VERSION )
                    ui32Error = m_pTraGizmo->load( pLoad );
                break;

            case CHUNK_IMAGEEFFECT_ATTRIBGIZMO:
                // Attribute
                if( pLoad->get_chunk_version() == IMAGEEFFECT_VERSION )
                    ui32Error = m_pAttGizmo->load( pLoad );
                break;

            default:
                assert( 0 );
            }

            pLoad->close_chunk();

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

        return ui32Error;
    }

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