#!/usr/bin/perl
# Extracts the raw pixel data from a HP48 calculator screenshot dump as
#   obtained from the serial line when pressing ON-1.
# The input file must be 1080 bytes in size.
# The output will be a raw image that can be opened in GIMP as a .data file,
#   in 1-bit format, width 131 and height 64.
# Or if you want to use ImageMagick: invoke with -wr, and convert the result:
#   convert -size 131x64 -depth 1 -endian MSB MONO:output.data output.png
#
# Hint: if you have installed the 'pyserial' module, a simple way to grab
# the screenshot from a USB serial device is:
#   python -m serial.tools.miniterm -f direct --raw /dev/cu.usbserial-yadda 9600 > screen.hp
# Press ctrl-] in this console after pressing ON-1 on the calculator.
#
# Alexander Thomas, 2021/04/17.
# Inspired by the source of the old HP2TIFF and HP2BMP programs.
# Released under a Creative Commons CC0 1.0 Universal License
#   (in other words, public domain).
#   https://creativecommons.org/publicdomain/zero/1.0/

use strict;
use warnings;

sub usage
{
	print "Usage: $0 [-wr] inputFile output.data\n".
	      "  -w: wide output (136x64 pixels) to avoid chopping up bytes.\n".
	      "  -r: reverse bit order per byte.\n";
}

my ($wide, $reverse);
while(defined($ARGV[0]) && $ARGV[0] =~ /^-/) {
	my $arg = shift;
	my @switches = split(//, $arg);
	shift(@switches);
	foreach my $sw (@switches) {
		if($sw eq 'h') {
			usage();
			exit(0);
		}
		elsif($sw eq 'w') {
			$wide = 1;
		}
		elsif($sw eq 'r') {
			$reverse = 1;
		}
		else {
			print STDERR "WARNING: ignoring unknown switch '${sw}'\n";
		}
	}
}

my $inFile = shift;
my $outFile = shift;

if(! $outFile) {
	usage();
	exit 2;
}

open(my $inHandle, '<', $inFile) or die "Cannot open '${inFile}': $!\n";
binmode $inHandle;

my $startCode = pack('C*', 27, 131);
my $crlf = pack('C*', 13, 10);
my ($code, $blob);

my @imgData;

for(my $row = 0; $row < 8; $row++) {
	read($inHandle, $code, 2) or die "SNAFU while reading input: $!";
	die "Input file does not contain expected marker bytes.\n" if($code ne $startCode);
	read($inHandle, $blob, 131) or die "SNAFU while reading input: $!";
	push(@imgData, unpack('C*', $blob));
	read($inHandle, $code, 2) or die "SNAFU while reading input: $!";
	print "Warning: no CRLF at expected location, ignoring.\n" if($code ne $crlf);
}
close($inHandle);

# This is based on source code from HP2BMP, which was based on HP2TIFF.
# They work with a 160x64 image (or 20x64 bytes) to make calculations much simpler.
# I shrunk this down to 136x64 (or 17x64 bytes).
# I haven't figured out how exactly the format works, but this does the trick.
my @outImage = (0) x 1088;

for(my $i = 0; $i < 8; $i++) {
	for(my $j = 0; $j < 8; $j++) {
		for(my $k = 0; $k < 131; $k++) {
			$outImage[($i * 8 + $j) * 17 + int($k / 8)] |=
				(($imgData[131 * $i + $k] & (1 << $j)) >> $j) << (7-($k % 8));
		}
	}
}

my @finalImage;
if($wide) {
	@finalImage = @outImage;
}
else {
	# Crop it to 131x64 to get a ready-to-use result.
	# Because it's a 1-bit image, this requires bit shifting.
	@finalImage = (0) x 1048;
	for(my $j = 0; $j < 64; $j++) {
		my $shift = (131 * $j) % 8;
		if($shift == 0) {
			for(my $i = 0; $i <= 16; $i++) {
				$finalImage[int(131 * $j / 8) + $i] = $outImage[17 * $j + $i];
			}
			next;
		}
		for(my $i = 0; $i <= 16; $i++) {
			my $byte = $outImage[17 * $j + $i];
			my $outIdx = int(131 * $j / 8) + $i;
			$finalImage[$outIdx] |= $byte >> $shift;
			$finalImage[$outIdx + 1] |= ($byte << (8 - $shift)) % 256 unless($j == 63 && $i == 16);
		}
	}
}

# Invert it to eliminate any need for post-processing in GIMP.
@finalImage = map {255 - $_} @finalImage;

open(my $outHandle, '>', $outFile) or die "Cannot open '${outFile}' for writing: $!\n";
binmode $outHandle;
if($reverse) {
	print $outHandle map {pack('b8', unpack('B8', pack('C', $_)))} @finalImage;
}
else {
	print $outHandle pack('C*', @finalImage);
}
close($outHandle);
