Skip to content

Commit

Permalink
Added .ico file format reader (#105)
Browse files Browse the repository at this point in the history
  • Loading branch information
R32 authored Sep 6, 2022
1 parent ec1044c commit 318e49a
Show file tree
Hide file tree
Showing 5 changed files with 357 additions and 0 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ Currently supported formats are :
| GIF | Image file format |||
| GZ | Compressed file |||
| HL | HashLink |||
| ICO | Windows ICO/CUR File format |||
| JPG | Image file format |||
| LZ4 | Compressed file |||
| MP3 | Compressed audio |||
Expand Down
77 changes: 77 additions & 0 deletions format/ico/DIB.hx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package format.ico;

import format.ico.Data;

/**
* Device-Independent Bitmap
*/
class DIB {
public var data(default, null) : haxe.io.Bytes;
public var info(default, null) : BMPInfo;
public var ixor(default, null) : Int;
public var iand(default, null) : Int;
public var width(default, null) : Int;
public var height(default, null) : Int;
public var colors(default, null) : Array<Int>;
public function new( data, info ) {
this.data = data;
this.info = info;
this.width = info.width;
this.height = info.height >> 1;
this.ixor = info.findDIBBits();
this.iand = this.ixor + this.height * info.bytesPerline();
this.colors = [];
var ptr = info.sizeof;
var max = ptr + info.numberColors() * 4;
while(ptr < max) {
this.colors.push(data.getInt32(ptr));
ptr += 4;
}
}
}

/**
* For .ico file format, Only the following members are used:
*
* sizeof, width, height, planes, bitCount, sizeImage. All other members must be 0.
*/
class BMPInfo {
public var sizeof : Int; // DWORD, size of the structure = 40
public var width : Int; // DWORD, width of the image in pixels
public var height : Int; // DWORD, height of the image in pixels, (negative: top-down, positive: bottom-up)
public var planes : Int; // WORD, = 1
public var bitCount : Int; // WORD, bits per pixel (1, 4, 8, 16, 24, or 32)
public var compression : Int; // DWORD, compression code
public var sizeImage : Int; // DWORD, number of bytes in image
public var xPelsPerMeter : Int; // DWORD, horizontal resolution
public var yPelsPermeter : Int; // DWORD, vertical resolution
public var clrUsed : Int; // DWORD, number of colors used
public var clrImportant : Int; // DWORD, number of important colors

public function new() {
}

// Calculates the number of entries in the color table.
public function numberColors() : Int {
if (clrUsed > 0)
return clrUsed;
return switch(bitCount) { // 1 << bitCount
case 1: 2;
case 4: 16;
case 8: 256;
default: 0;
}
}

// Calculates the number of bytes in the color table.
public inline function paletteSize() return numberColors() * 4;

// Locate the image bits in a CF_DIB format DIB.
public inline function findDIBBits() return sizeof + paletteSize();

// Calculates the number of bytes in one scan line from Indexes
public inline function bytesPerline() return WIDTHBYTES(width * bitCount);

// Multiple of 4
static public inline function WIDTHBYTES( bits ) return bits + 31 >> 5 << 2;
}
38 changes: 38 additions & 0 deletions format/ico/Data.hx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package format.ico;

import format.ico.DIB;

enum Data {
DIB( d : DIB );
PNG( b : haxe.io.Bytes );
}

class ICORoot {
public var reserved : Int; // WORD, Reserved (must be 0)
public var type : ICOType; // WORD
public var count : Int; // WORD, length of entries
public var entries : Array<ICOEntry>;
public var datas : Array<Data>;
public function new() {
entries = [];
datas = [];
}
}

class ICOEntry {
public var width : Int; // BYTE
public var height : Int; // BYTE
public var colorCount : Int; // BYTE
public var reserved : Int; // BYTE
public var planes : Int; // WORD, (x hotspot if CURSOR)
public var bitCount : Int; // WORD, (y hotspot if CURSOR)
public var size : Int; // DWORD
public var offset : Int; // DWORD
public function new() {
}
}

extern enum abstract ICOType(Int) to Int {
var ICON = 1;
var CURSOR = 2;
}
71 changes: 71 additions & 0 deletions format/ico/Reader.hx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package format.ico;

import format.ico.Data;
import format.ico.DIB;

class Reader {

var input : haxe.io.Input;

inline function readUInt16() return input.readUInt16();
inline function readInt32() return input.readInt32();
inline function readByte() return input.readByte();

public function new(i) {
input = i;
input.bigEndian = false;
}

public function read() : ICORoot {
var root = new ICORoot();
root.reserved = readUInt16();
root.type = cast readUInt16();
root.count = readUInt16();

if (root.reserved != 0)
return root; // empty

for (i in 0...root.count) {
var entry = new ICOEntry();
entry.width = readByte();
entry.height = readByte();
entry.colorCount = readByte();
entry.reserved = readByte();
entry.planes = readUInt16();
entry.bitCount = readUInt16();
entry.size = readInt32();
entry.offset = readInt32();
root.entries.push(entry);
}

for (entry in root.entries) {
var bmp = haxe.io.Bytes.alloc(entry.size);
input.readBytes(bmp, 0, entry.size);

if (bmp.getInt32(0) == PNG_SIGN) {
root.datas.push( PNG(bmp) );
continue;
}

var info = new BMPInfo();
info.sizeof = bmp.getInt32(0);
info.width = bmp.getInt32(1 * 4);
info.height = bmp.getInt32(2 * 4);
info.planes = bmp.getUInt16(3 * 4);
info.bitCount = bmp.getUInt16(3 * 4 + 2);
info.compression = bmp.getInt32(4 * 4);
info.sizeImage = bmp.getInt32(5 * 4);
info.xPelsPerMeter = bmp.getInt32(6 * 4);
info.yPelsPermeter = bmp.getInt32(7 * 4);
info.clrUsed = bmp.getInt32(8 * 4);
info.clrImportant = bmp.getInt32(9 * 4);

var image = new DIB(bmp, info);
root.datas.push(DIB(image));
}
input = null;
return root;
}

static inline var PNG_SIGN = 0x89 | ("P".code << 8) | ("N".code << 16) | ("G".code << 24);
}
170 changes: 170 additions & 0 deletions format/ico/Tools.hx
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
package format.ico;

import format.ico.Data;
import format.ico.DIB;

class Tools {

/**
* extract DIB to [R, G, B, A, ...] format
*/
static public function extract( bmp : DIB ) : Uint8Array {
// fast reference
var ixor = bmp.ixor;
var source = bmp.data;
var height = bmp.height;
var colors = bmp.colors;

var stride = bmp.info.bytesPerline();
var output = new Uint8Array(bmp.width * height * 4);
var byteWidth = (bmp.width * bmp.info.bitCount) + 7 >> 3;
var index = 0;
var column : Int;
var starts : Int;

switch (bmp.info.bitCount >> 2) { // shrink switch table
case 0: // 1bits
var iand = bmp.iand;
var rowlen = bmp.width * 4;
var limits = rowlen;
while (--height >= 0) {
starts = height * stride;
column = 0;
while (column < byteWidth) {
var pos = starts + column++;
var icx = source.get(ixor + pos);
var mak = source.get(iand + pos);
var bits = 8;
while (--bits >= 0 && index < limits) {
var rgb = colors[icx >> bits & 1];
output[index++] = rgb >> 16 & 0xFF;
output[index++] = rgb >> 8 & 0xFF;
output[index++] = rgb & 0xFF;
output[index++] = (mak >> bits & 1) - 1;
}
}
limits += rowlen;
}
case 1: // 4bits
copyAlphaChannel(output, bmp);
while (--height >= 0) {
starts = ixor + height * stride;
column = 0;
while (column < byteWidth) {
var icx = source.get(starts + column++);
writeRGB(output, index, colors[(icx >> 4) & 15]);
index += 4;

// if the iamge width is not multiple of 2
if (column == byteWidth && (byteWidth & 1) == 1)
break;

writeRGB(output, index, colors[(icx ) & 15]);
index += 4;
}
}
case 2: // 8bits
copyAlphaChannel(output, bmp);
while (--height >= 0) {
starts = ixor + height * stride;
column = 0;
while (column < byteWidth) {
var icx = source.get(starts + column++);
writeRGB(output, index, colors[icx]);
index += 4;
}
}
case 6: // 24bits with 1 alpha bit
copyAlphaChannel(output, bmp);
while (--height >= 0) {
starts = ixor + height * stride;
column = 0;
while (column < byteWidth) {
output[index ] = source.get(starts + column + 2);
output[index + 1] = source.get(starts + column + 1);
output[index + 2] = source.get(starts + column );
index += 4;
column += 3;
}
}
case 8: // 32bits, NOTE: it still has the unused alpha table in "bmp.iand"
while (--height >= 0) {
starts = ixor + height * stride;
column = 0;
while (column < byteWidth) {
var argb = source.getInt32(starts + column);
output[index++] = (argb >> 16) & 0xFF;
output[index++] = (argb >> 8) & 0xFF;
output[index++] = (argb ) & 0xFF;
output[index++] = (argb >> 24) & 0xFF;
column += 4;
}
}
default:
}
return output;
}

static function writeRGB( output : Uint8Array, i : Int, rgb : Int ) : Void {
if (output[i + 3] == 0)
return;
output[i++] = (rgb >> 16) & 0xFF;
output[i++] = (rgb >> 8) & 0xFF;
output[i] = rgb & 0xFF;
}

static function copyAlphaChannel( output : Uint8Array, bmp : DIB ) : Void {
var data = bmp.data;
var iand = bmp.iand;
var stride = BMPInfo.WIDTHBYTES(bmp.width);
var rowlen = bmp.width * 4;
var height = bmp.height;
var limits = rowlen;
var i = 3;
while (--height >= 0) {
var column = 0;
var starts = iand + height * stride;
while (column < stride) {
var mark = data.get(starts + column++);
var bits = 8;
while (--bits >= 0 && i < limits) {
output[i] = ((mark >> bits) & 1) - 1; // if (1) then 0 else -1
i += 4;
}
}
limits += rowlen;
}
}
}

#if js
typedef Uint8ArrayInner = js.lib.Uint8Array;
#elseif hl
typedef Uint8ArrayInner = hl.Bytes;
#else
typedef Uint8ArrayInner = haxe.io.Bytes;
#end

@:pure
@:forward(length)
extern abstract Uint8Array(Uint8ArrayInner) to Uint8ArrayInner {
#if (js || hl)
inline function new( size : Int ) this = new Uint8ArrayInner(size);

@:arrayAccess inline private function get( pos : Int ) : Int return this[pos];

@:arrayAccess inline private function set( pos : Int, value : Int ) : Int return this[pos] = value;

#else
inline function new( size : Int ) this = haxe.io.Bytes.alloc(size);

@:arrayAccess inline private function get( pos : Int ) : Int {
return this.get(pos);
}

@:arrayAccess inline private function set( pos : Int, value : Int ) : Int {
this.set(pos, value);
return value;
}
#end
}

0 comments on commit 318e49a

Please sign in to comment.