Opening Paint files in XL

The Great Wave of Kanagawa
Specifically, MacPaint files. Why MacPaint? 1. With a maximum size of 576 pixels wide by 720 pixels tall, MacPaint images fit on a modern spreadsheet when using cells as pixels, and 2. MacPaint prototyped the lossless PackBits compression algorithm, which is the default compression scheme for TIFF.

PackBits is a signed-byte file structure with flag-counter bytes interspersed with data bytes. If the flagbyte is negative, the next byte (always a data byte) is repeated, zero-based, the two’s-complement of the flagbyte times. If the flagbyte is a positive number, the next number of bytes are copied directly. Each data byte represents the status of eight pixels, with 1 indicating a black pixel and 0 representing a white pixel, as shown below.

The PackBits algorithm is defined here. The following code, cross-posted to Wikipedia, handles Apple’s demonstration case.

Sub UnpackBitsDemo()

   Dim File    As Variant
   Dim MyOutput As String
   Dim Count   As Long
   Dim i As Long, j As Long
 
   File = “FE AA 02 80 00 2A FD AA 03 80 00 2A 22 F7 AA”
   File = Split(File, ” “)
   
   For i = LBound(File) To UBound(File)
      Count = Application.WorksheetFunction.Hex2Dec(File(i))
      Select Case Count
      Case Is >= 128
         Count = 256 – Count ‘Two’s Complement
         For j = 0 To Count ‘zero-based
            MyOutput = MyOutput & File(i + 1) & ” “
         Next j
         i = i + 1 ‘Adjust the pointer
      Case Else
         For j = 0 To Count ‘zero-based
            MyOutput = MyOutput & File(i + j + 1) & ” “
         Next j
         i = i + j ‘Adjust the pointer
      End Select
   Next i
   Debug.Print MyOutput
   ‘AA AA AA 80 00 2A AA AA AA AA 80 00 2A 22 AA AA AA AA AA AA AA AA AA AA’
End Sub

It works out then that a PackBits stream is a flagbyte followed by data bytes followed by a flagbyte followed by data bytes… Each flagbyte indirectly points to the location of the next flagbyte.
 
Excel’s VBA does not have signed bytes, but if we look at the flagbyte’s value, and “Select Case” all flagbytes greater or equal to 128 (b10000000) we have a test that handles negative flag bytes. The two’s complement of a number N is 2B-N, where B is the word size, in our case, 8. In other words, the two’s-complement of the negative flagbyte is 256 minus the value of the flagbyte.
 
The MacPaint file has a 512-byte header that we can ignore. It also can have, as data bytes, internal bytes that would otherwise be read as end-of-file. We handle that by reading the file in for binary access.
 
The basic approach is to read in the file similarly to a text file. Turn the flagbytes characters into ascii, with the ascii as the count to copy or to take the two’s complement of. Byte(513) has to be a flagbyte. The data bytes are input as characters, turned to ascii, then turned to byte-strings. The cells are turned black or white by looping through the byte-string. After 576 1’s and 0’s (or 72 data bytes) a new row is started. The file structure does not reveal the length of the data, but we can infer that from the file’s length.

Sub OpenMacPaint()
   Dim TotalBytes As Long
   Dim Buffer As String
   Dim File    As Variant
   Dim Char  As String * 1
   Dim NextChar As String * 1
   Dim NextInt As Integer
   Dim NextByte As String * 8
   Dim Count   As Long
   Dim i As Long, j As Long, r As Long, c As Long, b As Long
   Dim Rng As Range
   
   File = Application.InputBox(“Enter the full path to the MacPaint file.”, “Path to the MacPaint file…”, _
      “Macintosh HD:Users:User:Downloads:GREAT WAVE.mac”, , , 2) ‘ Your path here
   If File = False Then Exit Sub

   Open File For Binary Access Read As #1
   TotalBytes = FileLen(File)
   Buffer = Input(TotalBytes, #1)
   Close #1
   If TotalBytes = 0 Then
      MsgBox “Exiting!”, vbCritical + vbOKOnly, “File not found!”
      Exit Sub
   End If
   
   c = 1
   r = 1
   Application.ScreenUpdating = False
   For i = 513 To TotalBytes ‘skip the header
      Char = VBA.Mid$(Buffer, i, 1)
      Count = Asc(Char)
      Select Case Count
         Case Is >= 128
            Count = 256 – Count ‘Two’s Complement
            NextChar = VBA.Mid$(Buffer, i + 1, 1)
            NextInt = Asc(NextChar)
            NextByte = Application.WorksheetFunction.Dec2Bin(NextInt, 8)
            For j = 0 To Count ‘zero-based repeat of the next byte
                For b = 1 To 8
                    If VBA.Mid$(NextByte, b, 1) = “1” Then
                        Worksheets(“Sheet4”).Cells(r, c).Interior.ColorIndex = 1 ‘Black
                    Else
                        Worksheets(“Sheet4”).Cells(r, c).Interior.ColorIndex = 2 ‘White
                    End If
                    c = c + 1
                    If c > 576 Then ‘a new row
                        c = 1
                        r = r + 1
                    End If
                Next b
            Next j
            i = i + 1 ‘adjust the counter
         Case Else
            For j = 0 To Count ‘zero-based copy of Count bytes
                NextChar = VBA.Mid$(Buffer, i + j + 1, 1)
                NextInt = Asc(NextChar)
                NextByte = Application.WorksheetFunction.Dec2Bin(NextInt, 8)
                For b = 1 To 8
                    If VBA.Mid$(NextByte, b, 1) = “1” Then
                        Worksheets(“Sheet4”).Cells(r, c).Interior.ColorIndex = 1
                    Else
                        Worksheets(“Sheet4”).Cells(r, c).Interior.ColorIndex = 2
                    End If
                    c = c + 1
                    If c > 576 Then ‘a new row
                         c = 1
                         r = r + 1
                    End If
                Next b
            Next j
            i = i + j ‘adjust the counter
         End Select
   Next i
   
   Set Rng = Worksheets(“Sheet4”).Range(“A1:VE720”)
   Rng.ColumnWidth = 1
   Rng.RowHeight = 12
   ActiveWindow.Zoom = 10
   Application.ScreenUpdating = True
Exit Sub

What I don’t like about the code is that for the case of Count>=128, the repetitive bytes, I’m parsing it Count times. Rather I should parse it once and copy it Count times. I just don’t see how. I know somebody out there does.
 
If you want the MacPaint copy of Hokusai’s The Great Wave off Kanagawa shown above, it’s here. A nice thing about the Excel code is that you can change the color indices to suit. An appropriate blue might be color index 32, RBG(0,0,255).

To show why PackBits is still used, the lossless JPEG used above for HTML purposes is 211 Kbytes. The lossless Mac counterpart is 17 Kbytes. While a MacPaint file can define 576*720 pixels (0.4 megapixels), its maximum size is 512 + (72*90) bytes, or just over 51 Kbytes.

 
…mrt

Posted in Uncategorized

5 thoughts on “Opening Paint files in XL

  1. Nice. I put zeros and ones in the cells and used conditional formatting to color the cells. I also moved the byte loop into a separate procedure. It takes about 5 seconds on my machine.

    Sub OpenMacPaint()
       Dim TotalBytes As Long
       Dim Buffer As String
       Dim File    As String
       Dim Char  As String * 1
       Dim NextChar As String * 1
       Dim NextInt As Integer
       Dim NextByte As String * 8
       Dim Count   As Long
       Dim i As Long, j As Long, r As Long, c As Long
       Dim Rng As Range
       Dim aOutput() As Long
       Dim sh As Worksheet
       
       Set sh = ActiveWorkbook.Worksheets.Add
       File = “C:UsersDickDownloadsGREAT WAVE.mac” ‘ Your path here

       Open File For Binary Access Read As #1
       TotalBytes = FileLen(File)
       Buffer = Input(TotalBytes, #1)
       Close #1
       If TotalBytes = 0 Then
          MsgBox “Exiting!”, vbCritical + vbOKOnly, “File not found!”
          Exit Sub
       End If
       
       ReDim aOutput(1 To 720, 1 To 576)
       
       c = 1: r = 1
       Application.ScreenUpdating = False
       For i = 513 To TotalBytes ‘skip the header
         Char = VBA.Mid$(Buffer, i, 1)
          Count = Asc(Char)
          Select Case Count
             Case Is >= 128
                Count = 256 – Count ‘Two’s Complement
               NextChar = VBA.Mid$(Buffer, i + 1, 1)
                NextInt = Asc(NextChar)
                NextByte = Application.WorksheetFunction.Dec2Bin(NextInt, 8)
                For j = 0 To Count ‘zero-based repeat of the next byte
                   FillArray aOutput, c, r, NextByte
                Next j
                i = i + 1 ‘adjust the counter
            Case Else
                For j = 0 To Count ‘zero-based copy of Count bytes
                   NextChar = VBA.Mid$(Buffer, i + j + 1, 1)
                    NextInt = Asc(NextChar)
                    NextByte = Application.WorksheetFunction.Dec2Bin(NextInt, 8)
                    FillArray aOutput, c, r, NextByte
                Next j
                i = i + j ‘adjust the counter
            End Select
       Next i
       
       Set Rng = sh.Range(“A1”).Resize(UBound(aOutput, 1), UBound(aOutput, 2))
       Rng.Value = aOutput
       Rng.FormatConditions.Add(xlCellValue, xlEqual, 1).Interior.Color = vbBlue
       Rng.FormatConditions.Add(xlCellValue, xlEqual, 0).Interior.Color = vbWhite
       Rng.ColumnWidth = 1
       Rng.RowHeight = 12
       ActiveWindow.Zoom = 10
       Application.ScreenUpdating = True
       
    End Sub

    Sub FillArray(ByRef aOutput() As Long, ByRef c As Long, ByRef r As Long, NextByte As String)
       
        Dim b As Long
       
        For b = 1 To 8
            aOutput(r, c) = Mid$(NextByte, b, 1)
            c = c + 1
            If c > 576 Then ‘a new row
                c = 1
                 r = r + 1
            End If
        Next b
       
    End Sub

  2. Hi Dick –

    Every accountant needs this one. The file date on my computer says that it’s been around since June 1986. Along the way, it’s picked up some extra bytes, meaning the inference from the filesize can’t be trusted. For my code, it just fills the extra five or so rows with the garbage it is. For your code, it runs out of bounds. The fixes are easy and similar. Under the line

    r = r + 1

    Put the line

    If r > 720 Then Exit For

    Two times for me, once for you. And above the line

    Next i

    Put the same line, once for each of us.

    Back when I was on the industrial side of the military/industrial complex, we had a concept of “team programming.” One senior software engineer would program with one junior engineer side-by-side. While the firm wouldn’t get the same SLOC out as from two engineers working separately, the quality of the SLOC was higher. That’s kind of what happened here, with me being the figurative junior engineer.

    Thanks for the insights.

    …mrt

  3. Hi Michael,

    I’m a regular reader of DDOE (not so regular contributor, sorry Dick). Nice to meet you, online that is.

    Just out of curiosity, how do you know of Hokusai? Or do you just like the picture?

    Andrew

  4. Hi Andrew –

    Mostly the later. Seems there are many copies. I saw it too long ago in the British Museum. From there, I learned the name. I don’t know any more about Hokusai than what I read in Wikipedia. I didn’t do the original scan, but I kept it because of the relation to a visit abroad. The scan is really a poor rendition as you have to know the picture to see the boats being swamped by the waves.

    It’s easy, mostly, to keep electronic souvenirs. This one was on an 800k floppy disc. I moved it onto newer technology while I could still resurrect a computer that would read 800k floppies and write 1.4M floppies. It was sneaker net at its finest. Read the 800k discs to the old computer’s hard drive, write to the 1.4M disc, USB floppy-drive copy to a USB/Firewire external hard drive to my laptop, erase the disc, and repeat. I set it on the kitchen table and just kept walking that disc around. Many files got corrupted at sometime along the way, but the two I linked to work great.

    Nice to meet you, too.

    …mrt

  5. Hi Michael,

    It certainly is a good picture and, many years after it was made, still a very popular one. I actualy have Hokusai’s 36 Views of Mount Fuji (actually 46) as my screensaver at work. It’s a great way to get an insight into what society in Japan was like at that particular time.

    Andrew

Leave a Reply

Your email address will not be published. Required fields are marked *