[Previous][Up][Next] | (#fcl-res)

## How to implement a new resource writer

Remark: | This chapter assumes you have some experience in using this library.

We'll see how to implement a writer for a new resource file format. A resource writer is a descendant of TAbstractResourceWriter, and it's usually implemented in a unit named namewriter, where name is file format name.

Suppose we must write a writer for file format foo; we could start with a unit like this:

    unit foowriter;

    {$MODE OBJFPC} {$H+}

    interface

    uses
      Classes, SysUtils, resource;

    type
      TFooResourceWriter = class (TAbstractResourceWriter)
      protected
        function GetExtensions : string; override;
        function GetDescription : string; override;
        procedure Write(aResources : TResources; aStream : TStream); override;
      public
        constructor Create; override;
      end;

    implementation

    function TFooResourceWriter.GetExtensions: string;
    begin

    end;

    function TFooResourceWriter.GetDescription: string;
    begin

    end;

    procedure TFooResourceWriter.Write(aResources: TResources; aStream: TStream);
    begin

    end;

    constructor TFooResourceWriter.Create;
    begin

    end;

    initialization
      TResources.RegisterWriter('.foo',TFooResourceWriter);

    end.

Note that in the initialization section, TFooResourceWriter is registered for extension .foo.

We must implement abstract methods of TAbstractResourceWriter. Let's start with the simpler ones, GetExtensions and GetDescription.

    function TFooResourceWriter.GetExtensions: string;
    begin
      Result:='.foo';
    end;

    function TFooResourceWriter.GetDescription: string;
    begin
      Result:='FOO resource writer';
    end;

Now let's see Write. This method must write all resources in the TResources object to the stream.

Suppose that our foo format is very simple:

- the header is made by a 4-byte signature (FOO*), a longword holding the number of resources in the file, and other 8 bytes of padding.

- each resource has a 16-byte header containing:

  - a longword for the resource type (only IDs are allowed for types)
  - a longword for the resource name (only IDs are allowed for names)
  - a longword for the language ID
  - a longword for the size of the resource data
- after the resource header immediately comes the resource data, possibly padded so that it ends on a 4 byte boundary.
- file format is little-endian

Our Write method could be something like this:

    procedure TFooResourceWriter.Write(aResources: TResources; aStream: TStream);
    var i : integer;
    begin
      WriteFileHeader(aStream,aResources);
      for i:=0 to aResources.Count-1 do
        WriteResource(aStream,aResources[i]);
    end;

So we must implement WriteFileHeader, which writes the 16-byte file header, and WriteResource, which writes a single resource with its header.

Let's start from the first one:

    procedure TFooResourceWriter.WriteFileHeader(aStream: TStream; aResources: TResources);
    var signature : array[0..3] of char;
        rescount : longword;
        padding : qword;
    begin
      signature:='FOO*';
      rescount:=aResources.Count;
      padding:=0;

      aStream.WriteBuffer(signature[0],4);
      aStream.WriteBuffer(rescount,sizeof(rescount));
      aStream.WriteBuffer(padding,sizeof(padding));
    end;

This code simply writes the file header as defined in foo format. Note that if we are running on a big endian system we should swap bytes before writing, e.g. calling SwapEndian function, but for simplicity this is omitted.

Now WriteResource comes. This method could be like this:

    procedure TFooResourceWriter.WriteResource(aStream: TStream; aResource: TAbstractResource);
    var aType : longword;
        aName : longword;
        aLangID : longword;
        aDataSize : longword;
    begin
      aType:=aResource._Type.ID;
      aName:=aResource.Name.ID;
      aLangID:=aResource.LangID;
      aDataSize:=aResource.RawData.Size;

      //write resource header
      aStream.WriteBuffer(aType,sizeof(aType));
      aStream.WriteBuffer(aName,sizeof(aName));
      aStream.WriteBuffer(aLangID,sizeof(aLangID));
      aStream.WriteBuffer(aDataSize,sizeof(aDataSize));

      //write resource data
      aResource.RawData.Position:=0;
      aStream.CopyFrom(aResource.RawData,aResource.RawData.Size);

      //align data so that it ends on a 4-byte boundary
      Align4Bytes(aStream);
    end;

We write the 16-byte resource header, and then resource data. Note that if resources have been loaded from a stream and the user didn't modify resource data, we are copying data directly from the original stream.

Align4Bytes is a private method (not shown for simplicity) that writes some padding bytes to the stream if needed, since FOO file format specifies that resource data must be padded to end on a 4 byte boundary. Again, we didn't consider endianess for simplicity. And finally, note that foo format only supports IDs for types and names, so if one of them is a name (that is, a string) this code might cause exceptions to be raised.

Note: More complex file formats store resources in a tree hierarchy; since TResources internally stores resources in this way too, a writer can choose to acquire a reference to the internal tree used by the TResources object (see TAbstractResourceWriter.GetTree) and use it directly. For these file formats resources can be written faster, since there is no overhead involved in building a separate resource tree in the writer.

That's all. Now you should be able to create a real resource writer.

Documentation generated on: May 14 2021