Using OS/2 2.x bitmap files

From EDM2
Jump to: navigation, search

Written by Timur Tabi

Introduction

This article discusses the OS/2 2.x bitmap file format and how to use it in your applications. OS/2 provides support for bitmaps contained in your executable's resources and they can be loaded with a single API call. However, there may be times when combining your bitmaps with the executable is not an option; unfortunately, OS/2 has no built-in support for bitmaps stored as .BMP files. This article provides the missing elements.

It should be noted that the information article will eventually be obsolete. Rumor has it that IBM is working on a providing built-in support for loading bitmaps, including those in other graphic formats like JPEG. There are also plans for a real image editor, something that is badly needed.

The bitmap file format is also used for icons and pointers, but these two are beyond the scope of this article. The PM Reference distinguishes between bitmaps and bit maps. Bitmaps are conventional bitmap images, the kind found in .BMP files, and they are the subject of this article. A bit map is either a bitmap, an icon, or a pointer.

Nota Bene: A lot of the information on the bitmap file format is either undocumented or so rarely used that no one outside IBM (including me) actually knows what it is. If anyone has any information on the bitmap file format that is not in this article (for instance, the halftoning algorithms), please let me know. I'll include it in my second edition!

The OS/2 2.x Bitmap File Format

OS/2 1.x used a bitmap file format very similar to the Windows 3.x DIB format. OS/2 2.x provides a newer format that offers more features like compression, different units of measurement, half-toning, different color encoding schemes, and user-defined space. Unfortunately, many of these advanced options are not documented and therefore rarely used.

OS/2 2.x supports both the old and the new bitmap formats. However, use of the old format is strongly discouraged, and this article is only concerned with the new format.

Bitmaps are divided into three parts: the bitmap information structure, the color table, and the actualy pel data.

Bitmap information structure

Structure BITMAPINFOHEADER2 contains information that defines the size and type of the bitmap, but it says nothing about the colors used or the actual image.

typedef struct _BITMAPINFOHEADER2 {
  ULONG cbFix;
  ULONG cx;
  ULONG cy;
  USHORT cPlanes;
  USHORT cBitCount;
  ULONG ulCompression;
  ULONG cbImage;
  ULONG cxResolution;
  ULONG cyResolution;
  ULONG cclrUsed;
  ULONG cclrImportant;
  USHORT usUnits;
  USHORT usReserved;
  USHORT usRecording;
  USHORT usRendering;
  ULONG cSize1;
  ULONG cSize2;
  ULONG ulColorEncoding;
  ULONG ulIdentifier;
} BITMAPINFOHEADER2;
cbFix
The size of the structure. Set it to sizeof(BITMAPINFOHEADER2).
[Editor's note - I do not normally do this, but since I know a fair amount about this topic, I will add that this is not necessarily true. The BITMAPINFOHEADER2 structure is defined semantically to be a variable-length structure meaning that you initialize up to the last field needed and set this field to be the size of the initialized portion of the structure. Thus, if you do not need any of the advanced features, you only initialize cx, cy, cPlanes, and cBitCount and set this field to 16, which is the sum of these fields' sizes.]
cx
The true width of the bitmap, in pixels
cy
The true height of the bitmap, in pixels
cPlanes
The number of colors planes. I've never seen this set to anything other than 1.
cBitCount
The number of bits/pixel, either 1, 4, 8, or 24.
ulCompression
The compression scheme used. These are documented in the PM Reference and can be any one of the following:
  • BCA_UNCOMP - Uncompressed
  • BCA_HUFFMAN1D - Huffman encoding, monochrome bitmaps only
  • BCA_RLE4 - Run-length encoded, 4 bits/pixel
  • BCA_RLE8 - RLE, 8/bit pixel
  • BCA_RLE24 - RLE, 24 bits/pixel
cbImage
The number of bytes that the pel data occupies. For an uncompressed image, this should be initialized to zero.
cxResolution
The horizontal resolution of the target device.
cyResolution
The vertical resolution of the target device.
cclrUsed
The number of colors in the color table. This field is used if the color table is smaller than the maximum number of colors. That is, if this is a 256-color bitmap, and the color table only defines 120 distinct colors in the bitmap, then this field is set to 120. If it's set to zero, then this is either a 24-bit bitmap of RGB values, or the color table is full-length.
cclrImportant
The number of colors in the color table that are required for satisfactory display of the bitmap. This field is used in case the system cannot map all of the colors in the table to the physical palette, so it tries to map only the first cclrImportant colors, and it approximates the rest.
usUnits
The units in which cxResolution and cyResolution are specified. Currently, only BRU_METRIC (pixels per meter) is supported.
usReserved
This is a reserved field and must be set to zero.
usRecording
The recording order of the pel data. The only supported value is BRA_BOTTOMUP, which means that the first row of pel data is the bottom row of the image.
usRendering
The halftoning algorithm for this image. None of these is documented anywhere.
  • BRH_NOTHALFTONED - This is the default.
  • BRH_ERRORDIFFUSION - (Damped) error diffusion.
  • BRH_PANDA - Processing Algorithm for Non-coded Document Acquisition.
  • BRH_SUPERCIRCLE - Super Circle.
cSize1
A parameter for the halftoning algorithm. It's not used if there's no halftoning.
cSize2
The second paramater for the PANDA and Super Circle algorithms.
ulColorEncoding
The type of values in the color table. The only supported value is BCE_RGB, which means the color table is an array of RGB2 structures.
ulIdentifier
Application-specific data. Use this ULONG for your own information. Great for storing pointers to additional data structures or functions, or even to C++ objects.

The Color Table

The color table is an array of RGB2 structures, which are defined in pmbitmap.h as:

typedef struct _RGB2 {
   BYTE bBlue;
   BYTE bGreen;
   BYTE bRed;
   BYTE fcOptions;
} RGB2;

The first three fields are the values of blue, red, and green, respectively. Each is an eight-bit value, and together they combine to form a 24-bit color value, which is the maximum that OS/2 can handle. There is also a 32-bit color standard, where another eight-bit value is used to indicate the transparency, but there is no support for this standard in OS/2.

The fourth field in RGB2 is the options field. There are two flags available, PC_RESERVED and PC_EXPLICIT. PC_RESERVED is used for animating colors with the GpiAnimatePalette() API. The PM Reference online documentation provides this cryptic exaplanation for PC_EXPLICIT:

"The low-order word of the color table entry designates a physical palette slot. This allows an application to show the actual contents of the device palette as realized for other logical palettes. This does not prevent the color in the slot from being changed for any reason."

I have yet to find any mention of this field in any other documentation. Fortunately, its meaning is not important for our uses - the fcOptions field is always set to zero.

The color table is nothing more than an array of RGB2 structures. The length of the array defines the number of colors in the bitmap. Ideally, the values of the color table will exactly match the current system palette. If not, OS/2 will automatically handle conversions. In either case, you don't have to worry about it.

The Bitmap Pel Data

The bitmap pel data is the actual bitmap data, stored as a two-dimensional array in row-major order. The dimensions of the bitmap are specified in the BITMAPINFOHEADER2 structure. Although a bitmap can be of any size, each row of data is padded to the nearest doubleword boundry. In other words, a 256-color bitmap (eight bits/pixel) that is five pixels wide would normally take five bytes for each row. In actuality, it takes up eight bytes per row, where the last three bytes are all zeros.

Each pixel is represented by an N-bit value in the pel data. A 256-color bitmap uses one byte for each pixel, and a 24-bit bitmap uses three bytes per pixel. The color table usually contains entries only for those colors used in the bitmap, although it may contain the entire palette. A 24-bit bitmap does not require a color table, although the Icon Editor tends to include one anyway.

Single and Multiple bitmap files

The OS/2 bitmap file format also supports multiple bitmaps per file. A single bitmap file is stored in this manner:

BITMAPFILEHEADER2
   BITMAPINFOHEADER2
Color table
Pel Data

BITMAPFILEHEADER2 is defined as:

typedef struct _BITMAPFILEHEADER2 {
   USHORT usType;
   ULONG cbSize;
   SHORT xHotspot;
   SHORT yHotspot;
   ULONG offBits;
   BITMAPINFOHEADER2 bmp2;
} BITMAPFILEHEADER2;
usType
The type of image, BFT_BMAP for our purposes
cbSize
Set to sizeof(BITMAPFILEHEADER2)

[Don't forget that the bmp2 field is a variable length structure. Adjust this value accordingly. - Editor]

xHotspot, yHotspot
The hotspot for icons and pointers, not used for bitmaps.
offBits
Points to the pel data for this bitmap.
bmp2
The BITMAPINFOHEADER2 structure as described in the section Bitmap information structure.

From this one structure, two out of the three parts of the bitmap are readily available - the info structure and the pel data. The third part, the color table, is located immediately after the info structure. The pointer manipulations are a bit tricky, but it only takes a few lines of code to get all three parts.

When multiple bitmaps are present, each is enclosed in a BITMAPARRAYFILEHEADER2 structure. For example, this is the layout of a bitmap file with two bitmaps:

BITMAPARRAYFILEHEADER2 (for bitmap #1)
   BITMAPFILEHEADER2
      BITMAPINFOHEADER2
   Color table
BITMAPARRAYFILEHEADER2 (for bitmap #2)
   BITMAPFILEHEADER2
      BITMAPINFOHEADER2
   Color table
   Pel Data (for bitmap #1)
   Pel Data (for bitmap #2)

The BITMAPARRAYFILEHEADER2 structure is described below.

typedef struct _BITMAPARRAYFILEHEADER2 {
   USHORT usType;
   ULONG cbSize;
   ULONG offNext;
   USHORT cxDisplay;
   USHORT cyDisplay;
   BITMAPFILEHEADER2 bfh2;
} BITMAPARRAYFILEHEADER2;
usType
Set to BFT_BITMAPARRAY, which indicates this as an array of bit maps.
cbSize
Set to sizeof(BITMAPARRAYFILEHEADER2)
offNext
Points to the next BITMAPARRAYFILEHEADER2, or 0 if this is the last one.
cxDisplay, cyDisplay
The resolution of the target display for this bitmap. See the section Using Multiple Bitmaps for an example of how to use these fields.
bfh2
The BITMAPFILEHEADER2 structure as described earlier in this section.

Creating a Bitmap from a .BMP file

Creating an OS/2 bitmap from a .BMP file requires three steps:

  1. Load the bitmap file into memory
  2. Locate the three main pieces of the bitmap file
  3. Use GpiCreateBitmap() to create a bitmap

The first part is easy. The following code opens the file, finds its length, allocates enough memory for it, and loads it into that memory.

#include <iostream.h>
#include <io.h>

#include <fcntl.h>
#include <malloc.h>

int load(const char *szName, char* &bitmap) {
  int fh=_open(szName,O_RDONLY | O_BINARY);
  if (fh == -1) {
    cout << "Error opening file " << szName << ".\n";
    return 0;
  }

  int length=_filelength(fh);
  if (length == -1) {
    cout << "Error determining length for " << szName << ".\n";
    _close(fh);
    return 0;
  }
  if (!length) {
    cout << szName << " has zero filesize.\n";
    _close(fh);
    return 0;
  }

  bitmap=(char *) malloc(length);
  if (!bitmap) {
    cout << "Error allocating " << length << " bytes.\n";
    _close(fh);
    return 0;
  }

  int bytesread=_read(fh,bitmap,length);

  if (!bytesread) {
    cout << "Read past end of file " << szName << ".\n";
    free(bitmap);
    _close(fh);
    return 0;
  }
  if (bytesread == -1) {
    cout << "Error reading file " << szName << ".\n";
    free(bitmap);
    _close(fh);
    return 0;
  }
  if (bytesread != length) {
    cout << "Could only read " << bytesread <<
         " of " << length << " bytes.\n";

    free(bitmap);
    _close(fh);
    return 0;
  }

  if (_close(fh)) {
    cout << "Error closing " << szName << ".\n";
    free(bitmap);
    return 0;
  }

  return length;
}

The first parameter is the name of the bitmap file to load. The second is a reference to a pointer to a block of memory. This function allocates the correct amount of memory itself and returns the pointer to that block. If all goes well, then the length of the block is returned. Otherwise, a zero indicates failure.

Parsing a Bitmap File

Once the bitmap is loaded into memory, its three subdivision must be located. The following code parses a single bitmap file:

void parse(PBITMAPFILEHEADER2 pbfh, PBYTE pbBmpFile) {
  PBITMAPINFOHEADER2 pbih=&pbfh->bmp2;
  PRGB2 prgb=((PBITMAPINFO2) pbih)->argbColor;
  PBYTE pbPelData=pbBmpFile+pbfh->offBits;
  hbm=makebmp(pbih,prgb,pbPelData);
}

Function parse() takes two parameters: a pointer to a BITMAPFILEHEADER2 structure and a pointer to the beginning of bitmap file. For a single bitmap file, these two are the same, but they are different for a multiple bitmap file.

The first line locates the bitmap info structure (BITMAPINFOHEADER2). The second line finds the color table, and the third line finds the pel data. The last line passes these three values to makebmp(), which actually creates the bitmap and is covered in the next section.

Making the Bitmap

Once the three parts of a bitmap are located, they need to be combined into a format acceptable by the GpiCreateBitmap() API. The following code does just that:

#define INCL_WINWINDOWMGR
#define INCL_GPIBITMAPS
#define INCL_DEV
#include <os2.h>

#include <malloc.h>
#include <memory.h>

static int ipow(int b, int e) {
  int p=b;
  while (--e) p*=b;
  return p;
}

HBITMAP makebmp(PBITMAPINFOHEADER2 pbih2, PRGB2 prgb2, PBYTE pbPelData) {

// Determine size of color table
  int iNumColors,numbits=pbih2->cPlanes * pbih2->cBitCount;
  if (numbits != 24)
    iNumColors = pbih2->cclrUsed ? pbih2->cclrUsed : ipow(2,numbits);
  else
    iNumColors = pbih2->cclrUsed;
  int iColorTableSize=iNumColors*sizeof(RGB2);

// Allocate storage for BITMAPINFO2

  PBITMAPINFO2 pbi2=(PBITMAPINFO2) malloc(sizeof(BITMAPINFOHEADER2)+iColorTableSize);

  if (!pbi2) return 0;

  memcpy(pbi2,pbih2,sizeof(BITMAPINFOHEADER2));
// Copy First half

  memcpy((PBYTE) pbi2+sizeof(BITMAPINFOHEADER2),prgb2,iColorTableSize);
//  Second half

  HPS hps=WinGetPS(HWND_DESKTOP);
  HBITMAP hbm=GpiCreateBitmap(hps,pbih2,CBM_INIT,pbPelData,pbi2);
  WinReleasePS(hps);
  free(pbi2);
  return hbm;
}

Function ipow() computes b^e, which is used to compute the size of the color table, in case the bitmap info structure doesn't specify it.

GpiCreateBitmap() doesn't use a BITMAPINFOHEADER2 structure. Instead, it uses a BITMAPINFO2 structure, which is nothing more than a BITMAPINFOHEADER2 followed by a color table. The bitmap file format always places the color table immediately after the corresponding BITMAPINFOHEADER2 structure, so you would think that there is no need to pass a BITMAPINFOHEADER2 and a color table. However, we want makebmp() to be generic enough to take bitmap data that didn't necessarily come from a bitmap file.

Function makebmp() performs the following tasks:

  • Determine the size of the color table
  • Allocate enough space to hold the BITMAPINFOHEADER2 and the color table
  • Copy both pieces to this block of data. This gives you a BITMAPINFO2.
  • Create a presentation space compatible with the display
  • Create the bitmap.
  • Release the presentation space and the allocated memory, since neither is needed any more.

Determining the size of the color table

If the bitmap is not a 24-bit bitmap, then there's a chance that the color table is 2^n entries long. If the cclrUsed field of the bitmap info structure is zero, then it is assumed that the table is full-length (i.e. 2^n entries long). Otherwise, there are cclrUsed entries.

If it is a 24-bit bitmap, then there cannot be 2^24 entries, so the cclrUsed field is guaranteed to contain the length of the color table. If this value is zero, then there is no color table. Technically speaking, a 24-bit bitmap doesn't need a color table at all.

Allocating the memory

Since a BITMAPINFO2 structure is equivalent to a a BITMAPINFOHEADER2 plus a color table, we need to allocated enough enough space for both. The size of a bitmap info structure is sizeof(BITMAPINFOHEADER2) [Remember the variable-length characteristic - Editor] and the size of the color table is equal to the number of entries times sizeof(RGB2).

Initializing the BITMAPINFO2 block

A pair of memcpy()'s, first for the BITMAPINFOHEADER2 and then for the color table right after it, is all it takes.

Creating the presentation space

In order to create a bitmap, OS/2 needs to know where you're planning on using it. Since we want to display these bitmaps on the screen, we need to create an appropriate presentation space. The easiest way to do this is to call WinGetPS() and pass it the handle of the desktop window.

Creating the bitmap

We now have everything we need. Just pass all the parameters to GpiCreateBitmap().

Cleanup

The bitmap that we create is self-sufficient, i.e. OS/2 makes a copy of all the data it needs, and the presentation space is only required during the creation, so we can release these two resources. That's it!

Using Multiple Bitmaps

If you've ever edited one of the standard icons that comes with OS/2, you'll notice there are several images defined. There's one that's 40x40 with 256 colors, one that's 32x32 with 16 colors, and several others. When OS/2 displays an icon, it searches the file for the best fit.

You can also use the same technique. Different bitmaps for different resolutions and colors can be created. OS/2 can scale and dither images automatically, but the results are often unsatisfactory. By defining a different bitmap for each resolution and color depth, you can always have perfect images.

Unfortunately, this approach is error-prone. Usually you need to define a set of bitmaps that go together. And for each bitmap in the set, you'll want one for each resolution. If you create one bitmap for a certain resolution, you'll create to do the same for all other bitmaps. If you don't, then you risk having your bitmaps mismatched.

Alternatively, you could have your program automatically scale the nearest match. But what is the nearest match? Which is closer to 800x600: 640x480 or 1024x768? And what if you're running 800x600 at 256 colors, and you have two bitmaps defined: 800x600x16 colors and 1024x768x256 colors. Which is more important, the resolution or the number of colors?

If I ever write a second edition for this article, I will address this issue in more detail.

The BMPINFO program

Included with this article is the source code to BMPINFO - a program which provides a detailed dump of all bit map files, including icons and pointers. This program is useful for testing programs which need to scan or create bitmap files. It can also be used to get a better understand of the bitmap file format. It supports single and multiple bitmaps, icons, and pointers in both the new and old bitmap file formats.

Note that many programs which create bitmap files might not initialize all the fields correctly. For instance, versions of JoeView prior to 1.22 would not set the cbSize field correctly, so BMPINFO could not determine the the version of the file format.