[personal profile] kpreid

I recently found I wanted to retrieve some old Apple IIgs image files. After vague memories and some research, I found that they were “Super Hi-Res” images, which are dumps of screen memory in SHR mode, passed through the PackBytes compression routine.

I haven't found any on-the-web documentation of the SHR layout, but according to books and my successful decoding, the pixel data starts at the beginning of the file, has 160 bytes per row, 200 rows, and no end-of-row data.

After the image data are 200 scanline control bytes (SCBs) and 16 color tables. I haven't looked at decoding these yet.

In 320 mode, each pixel is 4 bits specifying that color in a 16-position color table. In 640 mode, each pixel is 2 bits, specifying ((x-position mod 4) + pixel) in the color table; the default color table has black and white in the same position in each group of 4, and the same colors in the 1st and 3rd, and 2nd and 4th, subgroups of the color table. Thus, any pixel can be black or white (which was used for fonts) and pairs of pixels can be any of 15 distinct dithered colors (there are necessarily two grays).

Here's a program in C to decompress PackBytes format:

#include <stdio.h>

/* Decode the Apple IIGS PackBytes compression format. 

   Reads from stdin, writes to stdout, no error checking.

   Written with the help of:
     Ben Jackson <http://www.ben.com/>
     <http://www.fadden.com/techmisc/hdc/lesson02.htm> "Hacking Data Compression - Lesson 2"
     Apple IIGS Toolbox Reference, Volume 1 */

int main(void) {
  int header;
  
  while ((header = getchar()) != EOF) {
    int type = (header & 0xC0) >> 6;
    int count = (header & 0x3F) + 1;
    
    switch (type) {
      case 0:
        while (count--) putchar(getchar());
        break;
      case 1: {
        char what = getchar();
        while (count--) putchar(what);
        break;
      }
      case 2: {
        char whats[4] = {getchar(), getchar(), getchar(), getchar()};
        while (count--) {
          putchar(whats[0]);
          putchar(whats[1]);
          putchar(whats[2]);
          putchar(whats[3]);
        }
        break;
      }
      case 3: {
        char what = getchar();
        count *= 4;
        while (count--) {
          putchar(what);
        }
        break;
      }
    }
  }
  return 0;
}

This program, in Common Lisp, will convert an uncompressed SHR image to PPM:

(defconstant +width+ 320)
(defconstant +height+ 200)
(defconstant +color-tables+ 16)
(defconstant +colors+ 16)
(defconstant +channels+ 3)
(defconstant +levels+ 16)
(defconstant +pixels/byte+ 2)
(defconstant +bits/pixel+ (/ 8 +pixels/byte+))
(defconstant +space-after-scbs+ 56)

(defun pn (v)
  (prin1 v)
  (princ " "))

(defun read-shr (f)
  (with-open-file (stream f :direction :input :element-type '(unsigned-byte 8))
    (let ((image (make-array (list +width+ +height+) 
                             :element-type `(unsigned-byte ,+bits/pixel+)))
          (scbs  (make-array +height+
                            :element-type '(unsigned-byte 8)))
          (colors (make-array (list +color-tables+ +colors+ +channels+)
                              :element-type `(unsigned-byte ,+levels+))))
      (loop for y below +height+ do
        (loop for xhalf below (/ +width+ +pixels/byte+)
              for b = (read-byte stream)
              do
          (setf (aref image     (* xhalf +pixels/byte+)  y)
                  (ash b (- +bits/pixel+))
                (aref image (1+ (* xhalf +pixels/byte+)) y) 
                  (logand b #xF))))
      (let ((n (read-sequence scbs stream)))
        (assert (= n +height+)))
      (loop repeat +space-after-scbs+ do (read-byte stream))
      (loop for table below +color-tables+ do
        (loop for entry below +colors+
              for a = (read-byte stream)
              for b = (read-byte stream)
              do
          (setf (aref colors table entry 0) (logand a #xF)
                (aref colors table entry 1) (ash b -4)
                (aref colors table entry 2) (logand b #xF))))
      (assert (not (read-byte stream nil nil)))
      (values image scbs colors))))

(defun print-ppm-from-shr (file &key ww-colors)
  (map nil #'pn (list 'P3 +width+ +height+ (if ww-colors 2 15)))
  (multiple-value-bind (image scbs colors) (read-shr file)
    (when ww-colors
      (loop for i from 0 for (r g b) in
              '((0 0 0) ; empty
                (1 1 1) ; wire
                (0 0 1)
                (0 1 0)
                (0 1 1)
                (1 0 0)
                (2 1 0) ; electron tail
                (1 0 1)
                (1 2 2)
                (2 2 0) ; electron head
                (0 2 0) ; crossover
                (0 2 2) ; switch
                (2 0 0)
                (2 0 2)
                (0 0 2)
                (2 2 2)) 
            do (setf (aref colors 0 i 0) r
                     (aref colors 0 i 1) g
                     (aref colors 0 i 2) b)))
    (loop for y below +height+ do
      (loop for x below +width+
            for pixel = (aref image x y)
            do
        (loop for channel below 3 do
          (pn (aref colors (logand (aref scbs y) #xF) pixel channel))))))
  (fresh-line)
  (force-output))

The :ww-colors option exists because the particular files I wanted to convert were written by a program for running the WireWorld cellular automaton, which used a custom palette, but wrote its pattern files with the standard palette.