#!/usr/bin/env perl

## Copyright 2007, Lalit Chhabra (LC1178(at)yahoo.com)
## 
## Distributed under the Perl Artistic License at:
## http://www.perl.com/language/misc/Artistic.html
## See the end of this file for additional information.
## 
## This program converts a 24BPP BMP file produced by
## Imagemagicks "convert" program, into a 5:6:5 format
## BMP file. This is the format can be uploaded to the
## sandisk sansa mp3 player for viewing images.
##
## The imagemagick "convert" command for landscape 
## images is:
## convert <input img>  -geometry 224x176 -rotate 90 <outimg.bmp>
##
## and for portrait images is:
## convert <input img>  -geometry 176x224 <outimg.bmp>
##
## The sansa requires a 176x224 geometry. Also, the sansa
## does not seem to read the header at all, but we produce
## a correct header for other programs to display it
## correctly.

@commLine=split(/\//, $0);
$commFile=$commLine[$#commLine];
die "Usage: $commFile <input BMP file> <output BMP file>\n".
    "The input file should be in 24BPP, produced by Imagemagicks".
    " convert program.\n" if ($#ARGV < 1);

my($bmpFile) = $ARGV[0];
my($outFile) = $ARGV[1];

open(BMPIN, "$bmpFile") || die "Cannot open image file $bmpFile: $!\n";
open(BMPOUT, ">$outFile") || die "Cannot open file $outFile:$!\n";

binmode(BMPIN);
binmode(BMPOUT);

sysread(BMPIN, $head, 2);         ## read BM
my($fileSize) = readDword(BMPIN); ## read  4 bytes
my($tmp)      = readDword(BMPIN);
my($offset)   = readDword(BMPIN); ## upto byte 13
my($headSize) = readDword(BMPIN); ## upto byte 17
my($width)    = readDword(BMPIN); ## upto byte 21
my($height)   = readDword(BMPIN); ## upto byte 25
my($planes)   = readWord(BMPIN);  ## upto byte 27
my($bpp)      = readWord(BMPIN);  ## upto byte 29
my($compress) = readDword(BMPIN); ## upto byte 33
my($imgSize)  = readDword(BMPIN); ## upto byte 37
my($hRez)     = readDword(BMPIN); ## upto byte 41
my($vRez)     = readDword(BMPIN); ## upto byte 45
my($numCols)  = readDword(BMPIN); ## upto byte 49
my($impCols)  = readDword(BMPIN); ## upto byte 53

my($rMask, $gMask, $bMask);
if($compress == 3) {
  $rMask = readDword(BMPIN);
  $gMask = readDword(BMPIN);
  $bMask = readDword(BMPIN);
}

#printf("$head, $fileSize, $offset, $width x $height x $bpp\n", $fileSize);
#printf("$compress, $imgSize, $hRez, $vRez, $numCols, $impCols\n", $fileSize);
#print "$rMask, $gMask, $bMask\n";

## Calculate output image size
my($headerSize) = 66; ## Its $offset+12 for imagemagick
                      ## outputs, because $offset is 54 by
                      ## default.

## Calculate final row size in bytes. $width is the
## output (and input) pixels in a row. Each pixel is
## 2 bytes. The row size has to be a multiple of
## 4 bytes, and is null padded to make it so.
## So the row width in pixels has to be even.
my($rowSize) = ($width%2)? ($width+1)*2 : $width*2;

## Write output file
syswrite(BMPOUT, $head, 2);
syswrite(BMPOUT, makeDword($rowSize*$height+$headerSize), 4);
syswrite(BMPOUT, makeDword($tmp), 4);
syswrite(BMPOUT, makeDword($headerSize), 4);      ## Offset is header size
syswrite(BMPOUT, makeDword($headSize), 4);
syswrite(BMPOUT, makeDword($width), 4);
syswrite(BMPOUT, makeDword(-$height), 4);         ## We will make img upsidedown
syswrite(BMPOUT, makeWord($planes), 2);
syswrite(BMPOUT, makeWord(16), 2);                ## 16bpp
syswrite(BMPOUT, makeDword(3), 4);                ## Compression

syswrite(BMPOUT, makeDword($rowSize*$height), 4); ## imgsize for 16bpp
syswrite(BMPOUT, makeDword(0), 4);
syswrite(BMPOUT, makeDword(0), 4);
syswrite(BMPOUT, makeDword($numCols), 4);
syswrite(BMPOUT, makeDword($impCols), 4);

## Now write the 16bpp mask.
syswrite(BMPOUT, makeDword(0xf800), 4); # B mask
syswrite(BMPOUT, makeDword(0x7e0), 4);  # G mask
syswrite(BMPOUT, makeDword(0x1f), 4);   # R mask

## Go to the start of the image.
sysseek(BMPIN, $offset, 0);

## Read each row, and push into @rows.
my(@rows);
for(0..($height-1)) {
  my($row) = readRow(BMPIN, $width, $bpp);
  push(@rows, $row);
}

## Make the img upside-down. The sansa requires
## this. That is why the output image header has a 
## negative height (see header above).
for my $row (reverse(@rows)) {
  for(@$row) {
    syswrite(BMPOUT, pack("S", $_), 2);
  }
}

##----------------------------------------------------
## Read a doubleword (4 byte number) from the BMP file.
## The number is signed.
##----------------------------------------------------
sub readDword {
  my($fileHandle) = @_;

  my($temp);
  sysread($fileHandle, $temp, 4);
  ## Read it as a signed number.
  my($num) = unpack("i", $temp);
  return(abs($num));
}

##----------------------------------------------------
## Read a word (2 byte number) from the BMP file.
##----------------------------------------------------
sub readWord {
  my($fileHandle) = @_;

  my($temp0) = readByte($fileHandle);
  my($temp1) = readByte($fileHandle);

  my($aa) = ($temp1 << 8) + $temp0;
  return($aa);
}


##----------------------------------------------------
## Read a byte (1 byte number) from the BMP file.
##----------------------------------------------------
sub readByte {
  my($fileHandle) = @_;
  my($temp);
  sysread($fileHandle, $temp, 1);
  my($num) = unpack("c2", $temp);
  return($num);
}


##----------------------------------------------------
## Convert a number into a byte of binary data.
##----------------------------------------------------
sub makeByte {
  my($data) = @_;
  pack("C", $data & 0xff);
}

##----------------------------------------------------
## Convert a number into a word (2-bytes) of binary data.
##----------------------------------------------------
sub makeWord {
  my($data) = @_;
  my($lsb) = makeByte($data & 0xff);
  my($msb) = makeByte(($data>>8) & 0xff);
  return("$lsb$msb");
}

##----------------------------------------------------
## Convert a number into a dword (4-bytes) of binary data.
##----------------------------------------------------
sub makeDword {
  my($data) = @_;
  my($b0) = makeByte($data & 0xff);
  my($b1) = makeByte(($data>>8) & 0xff);
  my($b2) = makeByte(($data>>16) & 0xff);
  my($b3) = makeByte(($data>>24) & 0xff);
  return("$b0$b1$b2$b3");
}

##----------------------------------------------------
## Read $width pixels.
##----------------------------------------------------
sub readRow {
  my($fileHandle, $width, $bpp) = @_;

  ## BPP is assumed to be 24
  if($bpp != 24) {
     die "The script only works for input files in".
         " 24 BPP format. Please process with Imagemagicks".
         " convert program?\n";
  }

  my(@row);
  my($bytesPerPixel) = $bpp/8;
  my($temp);
  for my $pixel (0..($width-1)) {
    sysread($fileHandle, $temp, $bytesPerPixel);
    my($r, $g, $b) = unpack("H2H2H2", $temp);
    ($r, $g, $b) = (hex($r), hex($g), hex($b));
    my($bytes) = BPP16($r, $g, $b);
    push(@row, $bytes);
  }

  ## The number of bytes in a row has to be a
  ## multiple of 4. Since each pixel is 2 bytes,
  ## the number of output pixels has to be even.
  if($width%2) { push(@row, 0); }

  ## The input may also be padded with null
  ## bytes to make it have a multiple-of-four
  ## bytes. Read these extra bytes here, and
  ## discard them.
  my($bytesPerRow) = $bytesPerPixel * $width;
  my($nullBytes)   = (4 - ($bytesPerRow%4))%4;
  my($temp2);
  sysread($fileHandle, $temp2, $nullBytes);

  return(\@row);
}

##----------------------------------------------------
## Convert 3 bytes of RGB into 2 bytes of 5:6:5 format
## of B:G:R. The mask in the output file header controls
## where the B, G and R fields are. The translation
## is done by truncation. This may affect the quality,
## but is probably hard to visually detect.
##----------------------------------------------------
sub BPP16 {
  my($r, $g, $b) = @_;

  my($r16) =  $r>>3;
  my($g16) =  $g>>2;
  my($b16) =  $b>>3;
  return(($b16 << 11) | ($g16 << 5) | ($r16))
}


## LICENSE:
## Copyright (c) 2007 Lalit Chhabra. All rights reserved.  This program
## is free software; you can redistribute it and/or modify it under the
## same terms as Perl itself.  IN NO EVENT SHALL THE AUTHOR OR
## DISTRIBUTORS BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT, SPECIAL,
## INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OF THIS
## SOFTWARE, ITS DOCUMENTATION, OR ANY DERIVATIVES THEREOF, EVEN IF THE
## AUTHOR HAVE BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
## 
## THE AUTHOR AND DISTRIBUTORS SPECIFICALLY DISCLAIM ANY WARRANTIES,
## INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
## MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND
## NON-INFRINGEMENT. THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, AND
## THE AUTHOR AND DISTRIBUTORS HAVE NO OBLIGATION TO PROVIDE MAINTENANCE,
## SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.

