#!/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);