HomeForumSourceResearchGuide
Sign in to contribute to source. how it works
Component media.image.ImageEncoder:bmp by barry
expand copy to clipboardexpand
//all BMP files have this header:
const int BASIC_HDR_SIZE = 12
data BasicHeader {
	char ID[2]
	int4 size
	byte resvA[2]
	byte resvB[2]
	int4 data_offset
	}

//all BMP files then have another "DIB" header, the type of which varies between BMP types
// - the below is the most common Windows format
const int BMP_INFO_HDR_SIZE = 40
data BitmapInfoHeader {
	int4 hdr_size
	int4 width
	int4 height
	int2 color_panes
	int2 bits_per_pixel
	int4 compression_method
	int4 image_size
	int4 hres
	int4 vres
	int4 palette
	int4 imp_colors
	}

data Int4 {
	int4 n
	}

data Int2 {
	int2 n
	}

component provides ImageEncoder:bmp requires io.Output out, data.IntUtil iu {
	
	PixelMap map
	
	ImageEncoder:ImageEncoder()
		{
		}
	
	PixelMap ImageEncoder:getPixels()
		{
		return map
		}
	
	void ImageEncoder:setPixels(PixelMap pm)
		{
		map = pm
		}
	
	void reverseEndian(byte num[])
		{
		int j = num.arrayLength - 1
		for (int i = 0; i < num.arrayLength / 2; i++)
			{
			byte tmp = num[j]
			num[j] = num[i]
			num[i] = tmp
			j --
			}
		}
	
	int4 reverseInt4(int4 i4)
		{
		Int4 ct = new Int4(i4)
		reverseEndian(dana.serial(ct))
		return ct.n
		}
	
	int2 reverseInt2(int2 i2)
		{
		Int2 ct = new Int2(i2)
		reverseEndian(dana.serial(ct))
		return ct.n
		}
	
	bool ImageEncoder:loadImage(File fd)
		{
		//check for headers
		
		BasicHeader basicHeader = new BasicHeader()
		BitmapInfoHeader detailHeader = new BitmapInfoHeader()
		
		// read the basic header
		
		byte serial[] = dana.getByteArrayOf(basicHeader)
		
		byte in[] = fd.read(serial.arrayLength)
		
		if (in.arrayLength != serial.arrayLength) throw new Exception("unknown file format / IO read error")
		
		serial =[] in
		
		//check special bytes
		if (basicHeader.ID != "BM") throw new Exception("unknown file format")
		
		// - convert endianness
		basicHeader.size = reverseInt4(basicHeader.size)
		basicHeader.data_offset = reverseInt4(basicHeader.data_offset)
		
		// read the typical DIB header
		
		serial = dana.getByteArrayOf(detailHeader)
		
		in = fd.read(serial.arrayLength)
		
		if (in.arrayLength != serial.arrayLength) throw new Exception("unknown file format / IO read error")
		
		serial =[] in
		
		// - convert endianness
		detailHeader.hdr_size = reverseInt4(detailHeader.hdr_size)
		detailHeader.width = reverseInt4(detailHeader.width)
		detailHeader.height = reverseInt4(detailHeader.height)
		detailHeader.color_panes = reverseInt2(detailHeader.color_panes)
		detailHeader.bits_per_pixel = reverseInt2(detailHeader.bits_per_pixel)
		detailHeader.compression_method = reverseInt4(detailHeader.compression_method)
		detailHeader.image_size = reverseInt4(detailHeader.image_size)
		detailHeader.hres = reverseInt4(detailHeader.hres)
		detailHeader.vres = reverseInt4(detailHeader.vres)
		detailHeader.palette = reverseInt4(detailHeader.palette)
		detailHeader.imp_colors = reverseInt4(detailHeader.imp_colors)
		
		//check if we understand the header
		if (detailHeader.hdr_size != 40)
			throw new Exception("unrecognized format for bitmap")
		
		//check if we understand the compression format
		if (detailHeader.compression_method != 0)
			throw new Exception("unsupported format for bitmap (compressed)")
		
		//fill in commonly missing information
		if (basicHeader.data_offset == 0)
			basicHeader.data_offset = 24
		
		if (detailHeader.image_size == 0)
			detailHeader.image_size = detailHeader.width * detailHeader.height * 3
		
		//read the image data
		fd.setPos(basicHeader.data_offset)
		byte rawData[] = fd.read(detailHeader.image_size)
		
		if (detailHeader.bits_per_pixel == 24)
			{
			decodeData24bpp(detailHeader.width, detailHeader.height, rawData)
			}
			else
			{
			throw new Exception("unsupported format for this decoder")
			}
		
		return true
		}
	
	void decodeData24bpp(int width, int height, byte rawData[])
		{
		//bitmap data in this format is arranged as bgr, instead of rgba, so we convert assuming alpha is 255
		map = new PixelMap(new WH(width, height))
		map.pixels = new byte[width * height * 4]
		
		//bitmap data is stored bottom-up, so the last row is first
		
		//bitmaps use padding to ensure each row is a multiple of 4, so we calculate the padding amount (if any)
		int pad = 0
		if (((width * 3) % 4) != 0) pad = 4 - ((width * 3) % 4)
		
		for (int i = 0; i < height; i++)
			{
			int srcStartPixel = rawData.arrayLength - ((i+1) * ((width*3)+pad))
			int dstStartPixel = i * (width*4)
			
			for (int j = 0; j < width; j ++)
				{
				map.pixels[dstStartPixel] = rawData[srcStartPixel+2]
				map.pixels[dstStartPixel+1] = rawData[srcStartPixel+1]
				map.pixels[dstStartPixel+2] = rawData[srcStartPixel]
				map.pixels[dstStartPixel+3] = 255
				srcStartPixel += 3
				dstStartPixel += 4
				}
			}
		}
	
	bool ImageEncoder:saveImage(File fd)
		{
		if (map == null || map.size == null) throw new Exception("no image data to encode")
		if (map.size.width == 0 || map.size.height == 0) throw new Exception("image data has a zero dimension")
		
		//by default, we write file in BITMAPINFOHEADER/24bpp format
		
		//bitmaps use padding to ensure each row is a multiple of 4, so we calculate the padding amount (if any)
		int rowWidth = map.size.width * 3
		int pad = 0
		if (rowWidth % 4 != 0) pad = 4 - (rowWidth % 4)
		
		int dataSize = ((map.size.width * 3)+pad)*map.size.height
		
		BasicHeader basicHeader = new BasicHeader()
		BitmapInfoHeader detailHeader = new BitmapInfoHeader()
		
		// - write general header
		basicHeader.ID[0] = "B"
		basicHeader.ID[1] = "M"
		basicHeader.size = BASIC_HDR_SIZE + BMP_INFO_HDR_SIZE + dataSize
		basicHeader.data_offset = BASIC_HDR_SIZE + BMP_INFO_HDR_SIZE
		
		// (endianness)
		basicHeader.size = reverseInt4(basicHeader.size)
		basicHeader.data_offset = reverseInt4(basicHeader.data_offset)
		
		byte serial[] = dana.getByteArrayOf(basicHeader)
		
		fd.write(serial)
		
		// - write DIB format header
		detailHeader.hdr_size = BMP_INFO_HDR_SIZE
		detailHeader.width = map.size.width
		detailHeader.height = map.size.height
		detailHeader.color_panes = 1
		detailHeader.bits_per_pixel = 24
		detailHeader.compression_method = 0
		detailHeader.image_size = dataSize
		detailHeader.hres = 0
		detailHeader.vres = 0
		detailHeader.palette = 0
		detailHeader.imp_colors = 0
		
		// (endianness)
		detailHeader.hdr_size = reverseInt4(detailHeader.hdr_size)
		detailHeader.width = reverseInt4(detailHeader.width)
		detailHeader.height = reverseInt4(detailHeader.height)
		detailHeader.color_panes = reverseInt2(detailHeader.color_panes)
		detailHeader.bits_per_pixel = reverseInt2(detailHeader.bits_per_pixel)
		detailHeader.compression_method = reverseInt4(detailHeader.compression_method)
		detailHeader.image_size = reverseInt4(detailHeader.image_size)
		detailHeader.hres = reverseInt4(detailHeader.hres)
		detailHeader.vres = reverseInt4(detailHeader.vres)
		detailHeader.palette = reverseInt4(detailHeader.palette)
		detailHeader.imp_colors = reverseInt4(detailHeader.imp_colors)
		
		serial = dana.getByteArrayOf(detailHeader)
		
		fd.write(serial)
		
		// - write data
		byte encoded[] = new byte[dataSize]
		
		encodeData24bpp(map, encoded, pad)
		
		fd.write(encoded)
		
		return true
		}
	
	void encodeData24bpp(PixelMap m, byte rawData[], int pad)
		{
		//bitmap data is stored bottom-up, so the last row is first
		
		for (int i = 0; i < m.size.height; i++)
			{
			int srcStartPixel = rawData.arrayLength - ((i+1) * ((m.size.width*3)+pad))
			int dstStartPixel = i * (m.size.width*4)
			
			for (int j = 0; j < m.size.width; j ++)
				{
				rawData[srcStartPixel+2] = m.pixels[dstStartPixel]
				rawData[srcStartPixel+1] = m.pixels[dstStartPixel+1]
				rawData[srcStartPixel] = m.pixels[dstStartPixel+2]
				
				srcStartPixel += 3
				dstStartPixel += 4
				}
			}
		}
	
	}
Revision history
To propose a new revision to this entity, use dana source put -uc your/new/version.dn -n media.image.ImageEncoder:bmp -m "reason for update" -u yourUsername
Version 2 by barry
Version 1 (this version) by barry
Notes for this version: Standard Library Initialisation