arraysimageactionscript-3bitmapbmp

8 bit BMP image not generated when pixel array is too small


I am generating BMP images from RichEditableTexts using the Bitmap class to get the DisplayObject properties to manage it as pixels:

/*
 * Create a 8 bit BMP image as bytearray, with 256 color ( grayscale ).
 *
*/
private static function encode( bitmapData:BitmapData ):ByteArray {
    // bit depth configuration
    var bytesPerPixel:int = 1;
    var bitDepth:int = 8;

    // image/file properties
    var bmpWidth:int         = bitmapData.width;
    var bmpHeight:int        = bitmapData.height;
    var imageBytes:ByteArray = bitmapData.getPixels( bitmapData.rect );

    /* Image from Preview size */
    var imageSize:int = bmpWidth * bmpHeight * bytesPerPixel;

    /* Image offset */
    var imageDataOffset:int = 0x436;

    /* File size */
    var fileSize:int = imageSize + imageDataOffset;

    // binary BMP data
    var bmpBytes:ByteArray = new ByteArray();
    bmpBytes.endian = Endian.LITTLE_ENDIAN; // byte order

    // header information
    bmpBytes.length = fileSize;
    bmpBytes.writeByte(0x42);             // B                   //0
    bmpBytes.writeByte(0x4D);             // M (BMP identifier)  //1
    bmpBytes.writeInt(fileSize);          // file size           //2
    bmpBytes.position = 0x0A;             // offset to image data
    bmpBytes.writeInt( imageDataOffset );                        //10 4 Bytes
    bmpBytes.writeInt(0x28);              // header size         //14 4 Bytes
    bmpBytes.position = 0x12;             // width, height
    bmpBytes.writeInt( bmpWidth );                             //18 4 Bytes
    bmpBytes.writeInt( bmpHeight );                            //22 4 Bytes
    bmpBytes.writeShort( 1 );             // planes (1)        //26 2 Bytes
    bmpBytes.writeShort( bitDepth );      // color depth       //28 2 Bytes
    bmpBytes.writeInt( 0 );               // compression type  //30 4 Bytes
    bmpBytes.writeInt( imageSize );       // image data size   //34 4 Bytes

    bmpBytes.writeInt( 0x2E23 );          // Horizontal resolution //38 4 Bytes
    bmpBytes.writeInt( 0x2E23 );          // Vertical resolution   //42 4 Bytes

    bmpBytes.writeInt( 0x100 );           // Color in the palette

    bmpBytes.position = 0x36;             // start of color table

    /* COLOR TABLE */
    var table:uint = 256 * 4;
    for (var i:uint = 0; i < table; i++) {
        bmpBytes.writeByte( i ); //B
        bmpBytes.writeByte( i ); //G
        bmpBytes.writeByte( i ); //R
        bmpBytes.writeByte( 0 ); //A
        /*
         * Grays are made of equal bytes, for example: #AAAAAA is gray.
        */
    }

    bmpBytes.position = imageDataOffset; // start of image data... byte 310 // 1078

    // write pixel bytes in upside-down order
    // ( as per BMP format )
    var col:int = bmpWidth;
    var row:int = bmpHeight;
    var rowLength:int = col * bytesPerPixel; // Bytes per column based on Bit depth

    // Writing bytes to new image vars
    var writingOffset:int = 4 - ( bitDepth / 8 );

    try {
        // make sure we're starting at the
        // beginning of the image data
        imageBytes.position = 0;

        // Tmp ByteArray to extract 32 bits per pixel
        var tmpBytes:ByteArray;

        // bottom row up
        while (row--) {
            /* hey += "LINE\n"; */

            // from end of file up to imageDataOffset
            tmpBytes = new ByteArray();
            bmpBytes.position = imageDataOffset + ( row * rowLength );

            // read through each column writing
            // those bits to the image in normal
            // left to rightorder
            col = bmpWidth;
            while (col--) {
                // Extracting the 32 bits corresponding
                // to a pixel per getPixels method ( always the same ).
                imageBytes.readBytes( tmpBytes, 0, 4 );

                // We just need one BYTE of the 4 that are in this array.
                tmpBytes.position = 3;

                // THIS IS THE INDEX ON OUR COLOR TABLE ( GRAYSCALE ).
                bmpBytes.writeByte( tmpBytes.readUnsignedByte() );
            }
        }
    } catch(error:Error) {
        // end of file
        Alert.show( error.toString(), "I/O BMP ERROR" );
    }

    // return BMP file
    return bmpBytes;
}

DisplayObjects I am making images from:

RichEditableTexts

The first image is OK but the second isn't:

Windows Explorer

The second one opened with Atom:

enter image description here

Why does this happen?


Solution

  • I was not padding when needed. The resulting code:

    private static function encode( bitmapData:BitmapData ):ByteArray {
        var bytesPerPixel:int = 1;
        var bitDepth:int      = 8;
    
        // image/file properties
        var bmpWidth:int         = bitmapData.width;
        var bmpHeight:int        = bitmapData.height;
        var imageBytes:ByteArray = bitmapData.getPixels(bitmapData.rect);
        var imageSize:int        = imageBytes.length;
    
        // Offsets
        var imageDataOffset:int  = 0x436;
        var colorTableOffset:int = 0x36;
    
        // Pixel array
        var col:int = bmpWidth;
        var row:int = bmpHeight;
        var rowLength:int = col * bytesPerPixel; // 4 bytes per pixel (32 bit)
    
        // Padding
        var mod:int = ( rowLength % 4 ) != 0 ?  4 - ( rowLength % 4 ):0;
        rowLength += mod;
    
        // File size
        var fileSize:int = imageSize + imageDataOffset + ( mod * row );
    
        // binary BMP data
        var bmpBytes:ByteArray = new ByteArray();
        bmpBytes.endian        = Endian.LITTLE_ENDIAN; // byte order
    
        // header information
        bmpBytes.length = fileSize;
    
        // DIB header ( 40 bytes version )
        bmpBytes.writeByte(0x42);             // B
        bmpBytes.writeByte(0x4D);             // M (BMP identifier)
        bmpBytes.writeInt( fileSize );        // file size
        bmpBytes.position = 0x0A;             // offset to image data
        bmpBytes.writeInt(imageDataOffset);
        bmpBytes.writeInt(0x28);              // header size
        bmpBytes.position = 0x12;             // width, height
        bmpBytes.writeInt(bmpWidth);
        bmpBytes.writeInt(bmpHeight);
        bmpBytes.writeShort(1);               // planes (1)
        bmpBytes.writeShort(bitDepth);        // color depth (32 bit)
        bmpBytes.writeInt(0);                 // compression type
        bmpBytes.writeInt( ( imageSize + ( mod * row ) ) );         // image data size
    
        bmpBytes.writeInt( 0x2E23 );          // Horizontal resolution
        bmpBytes.writeInt( 0x2E23 );          // Vertical resolution
    
        bmpBytes.writeInt( 0x100 );           // Color in the palette
    
        bmpBytes.position = colorTableOffset; // start of color table...
    
        /* COLOR TABLE */
        for (var i:uint = 0; i < 1024; i++) {
            bmpBytes.writeByte( i ); //B
            bmpBytes.writeByte( i ); //G
            bmpBytes.writeByte( i ); //R
            bmpBytes.writeByte( 0 ); //A
        }
    
        /* Pixel array */
        bmpBytes.position = imageDataOffset;  // start of image data...
        var imgBmp:ByteArray;
        try {
    
            // make sure we're starting at the
            // beginning of the image data
            imageBytes.position = 0;
    
            // bottom row up
            while (row--) {
                imgBmp = new ByteArray();
    
                // from end of file up to imageDataOffset
                bmpBytes.position = imageDataOffset + row*rowLength;
    
                // read through each column writing
                // those bits to the image in normal
                // left to rightorder
                col = bmpWidth;
                while (col--) {
    
                    imageBytes.readBytes ( imgBmp, 0, 4 );
                    bmpBytes  .writeBytes( imgBmp, 1, 1 );
                }
                bmpBytes.position += mod;
            }
        }catch(error:Error){
            // end of file
            Alert.show( error.toString(), "EOF" );
        }
    
        // return BMP file
        return bmpBytes;
    }
    

    Per BMP specification, if bytes per row is not a multiple of four you have to add padding:

    Padding bytes (not necessarily 0) must be appended to the end of the rows in order to bring up the length of the rows to a multiple of four bytes. When the pixel array is loaded into memory, each row must begin at a memory address that is a multiple of 4. This address/offset restriction is mandatory only for Pixel Arrays loaded in memory. For file storage purposes, only the size of each row must be a multiple of 4 bytes while the file offset can be arbitrary.[5] A 24-bit bitmap with Width=1, would have 3 bytes of data per row (blue, green, red) and 1 byte of padding, while Width=2 would have 6 bytes of data and 2 bytes of padding, Width=3 would have 9 bytes of data and 3 bytes of padding, and Width=4 would have 12 bytes of data and no padding.

    I made this code simpler. At var mod:int = ( rowLength % 4 ) != 0 ? 4 - ( rowLength % 4 ):0; I adjust rowLenght depending on the actual rowLenght. Then I account for the padding (bmpBytes.position += mod;):

    /* Pixel array */
    bmpBytes.position = imageDataOffset;  // start of image data...
    var imgBmp:ByteArray;
    try {
    
        // make sure we're starting at the
        // beginning of the image data
        imageBytes.position = 0;
    
        // bottom row up
        while (row--) {
            imgBmp = new ByteArray();
    
            // from end of file up to imageDataOffset
            bmpBytes.position = imageDataOffset + row*rowLength;
    
            // read through each column writing
            // those bits to the image in normal
            // left to rightorder
            col = bmpWidth;
            while (col--) {
    
                imageBytes.readBytes ( imgBmp, 0, 4 );
                bmpBytes  .writeBytes( imgBmp, 1, 1 );
            }
            bmpBytes.position += mod;
        }
    }catch(error:Error){