SGI IMAGE HEADER

Greg Blair

  • We wrote this code to dump out the header from a SGI image file.
  • The SGI spec for version 0.97 of the SGI image file is included.

History

  • April 18, 2006 - initial release
  • April 19, 2006 fixed run-length table processing and dump

  ##+##########################################################################
  # 
  # printable -- Replaces all unprintable characters with dots.
  # history   -- lifted from sockspy.tcl
  # source    -- sockspy.tcl: https://wiki.tcl-lang.org/2256
  # 
  proc printable {s {spaces 0}} {
    regsub -all {[^\x09\x20-\x7e]} $s "." n
    if {$spaces} {
            regsub -all { } $n "_" n
    }
    return $n;
  }
  
  ##+##########################################################################
  # 
  # hexdump -- dump hex and ascii representation
  # author  -- Greg Blair
  # date    -- April 18, 2006
  # history -- patterned after sockspy.tcl's hex dumping code
  #            o added offset
  #            o print result
  # source  -- sockspy.tcl: https://wiki.tcl-lang.org/2256
  # 
  
  proc hexdump {data} {
    set offset 0
    while {[string length $data]} {
      set line [string range $data 0 15]
      set data [string range $data [string length $line] end]
      binary scan $line H* hex
      regsub -all {([0-9a-f][0-9a-f])} $hex {\1 } hex
      set line [format "%04x %-48.48s        %-16.16s" $offset $hex [printable $line 1]] 
      puts $line
      incr offset 16
    }
  }
  
  
  ##+##########################################################################
  # 
  # sgihdr -- read and parse sgi header fields
  # author -- Greg Blair
  # date   -- April 18, 2006
  # 
  proc sgihdr {file} {
    set fd [open $file r]
    if {$fd == ""} { error "cannot find file $file" }
    fconfigure $fd -translation binary
  
    #   2 bytes | short  | MAGIC     | IRIS image file magic number
    #   1 byte  | char   | STORAGE   | Storage format
    #   1 byte  | char   | BPC       | Number of bytes per pixel channel 
    #   2 bytes | ushort | DIMENSION | Number of dimensions
    #   2 bytes | ushort | XSIZE     | X size in pixels 
    #   2 bytes | ushort | YSIZE     | Y size in pixels 
    #   2 bytes | ushort | ZSIZE     | Number of channels
    #   4 bytes | long   | PIXMIN    | Minimum pixel value
    #   4 bytes | long   | PIXMAX    | Maximum pixel value
    #   4 bytes | char   | DUMMY     | Ignored
    #  80 bytes | char   | IMAGENAME | Image name
    #   4 bytes | long   | COLORMAP  | Colormap ID
    # 404 bytes | char   | DUMMY     | Ignored
    # ===
    # 512 byte header
  
    # binary scan field designators
    # S BIG ENDIAN signed 16 bit int (SGI MIPS processor byte order)
    # I BIG ENDIAN signed 32 bit int (SGI MIPS processor byte order)
    # b byte-low-to high bit order   (SGI MIPS processor bit  order)
    binary scan [read $fd 2] S magic
    if {$magic != 474} { close $fd; error "BAD MAGIC:$file is not an SGI file" }
    binary scan [read $fd 1] b storage
    binary scan [read $fd 1] b bpc
    binary scan [read $fd 2] S dimension
    binary scan [read $fd 2] S xsize
    binary scan [read $fd 2] S ysize
    binary scan [read $fd 2] S zsize
    binary scan [read $fd 4] I pixmin
    binary scan [read $fd 4] I pixmax
    set filler1 [read $fd 4]
    set imagename [read $fd 80]
    binary scan [read $fd 4] I colormapid
    set filler2 [read $fd 404]
  
    puts -nonewline "magic=$magic"
    puts -nonewline ",storage=$storage"
    if {$storage==1} {puts -nonewline " RLE"} else {puts -nonewline " UNCOMPRESSED"}
    puts -nonewline ",bpc=$bpc"
    puts -nonewline ",dimension=$dimension"
    puts -nonewline ",xsize=$xsize"
    puts -nonewline ",ysize=$ysize"
    puts -nonewline ",zsize=$zsize channels"
    puts -nonewline ",pixmin=$pixmin"
    puts -nonewline ",pixmax=$pixmax"
    puts ",colormapid=$colormapid"
    puts "zero byte terminated image name 79+1=80 bytes (not commonly used):"
    hexdump $imagename
    puts "4 byte filler1 (should be zeros):"
    hexdump $filler1
    puts "404 byte filler2 (should be zeros):"
    hexdump $filler2

    if {$storage == 1} { ;# RLE

      # read starttab
      for {set z 0} {$z < $zsize} {incr z} {
      for {set y 0} {$y < $ysize} {incr y} {
        binary scan [read $fd 4] I starttab($z,$y)
      }}
      
      # read lengthtab
      for {set z 0} {$z < $zsize} {incr z} {
      for {set y 0} {$y < $ysize} {incr y} {
        binary scan [read $fd 4] I lengthtab($z,$y)
      }}
      
      # print out row offset and row run length
      for {set z 0} {$z < $zsize} {incr z} {
      for {set y 0} {$y < $ysize} {incr y} {
        puts [format "chan %u row %4u offset %8u len %5u" $z $y $starttab($z,$y) $lengthtab($z,$y)]
      }}
    } ;# else VERBATUM
    puts "start of image data [tell $fd]"
    close $fd
  }
  
  if {$argc == 0} {
    set argv [glob *.sgi]
  }

  foreach file $argv {
    puts "$file:"
    sgihdr $file
    puts ""
  }
  
  if 0 {
   
                               Draft version  0.97
   
                           The SGI Image File Format
   
                                Paul Haeberli
   
                                 [email protected]
   
                       Silicon Graphics Computer Systems
   
   
   
   This is the definitive document describing the SGI image file format.  This 
   is a low level spec that describes the actual byte level format of SGI image
   files.  On SGI machines the preferred way of reading and writing SGI image
   files is to use the image library -limage.  This library provides a set
   of functions that make it easy to read and write SGI images.  If you are 
   on an SGI workstation you can get info on -limage by doing:
   
     % man 4 rgb
   
   A note on byte order of values in the SGI image files
   
     In the following description a notation like bits[7..0] is used to denote
     a range of bits in a binary value.   Bit 0 is the lowest order bit in
     the value.
   
     All short values are represented by 2 bytes.  The first byte stores the
     high order 8 bits of the value: bits[15..8].  The second byte stores
     the low order 8 bits of the value: bits[7..0].  
   
           So, this function will read a short value from the file:
   
               unsigned short getshort(inf)
               FILE *inf;
               {
                   unsigned char buf[2];
   
                   fread(buf,2,1,inf);
                   return (buf[0]<<8)+(buf[1]<<0);
               }
   
     All long values are represented by 4 bytes.  The first byte stores the
     high order 8 bits of the value: bits[31..24].  The second byte stores
     bits[23..16].  The third byte stores bits[15..8]. The forth byte stores
     the low order 8 bits of the value: bits[7..0].  
   
           So, this function will read a long value from the file:
   
               static long getlong(inf)
               FILE *inf;
               {
                   unsigned char buf[4];
   
                   fread(buf,4,1,inf);
                   return (buf[0]<<24)+(buf[1]<<16)+(buf[2]<<8)+(buf[3]<<0);
               }
   
   
   The general structure of an SGI image file is as shown below:
   
     The header indicates whether the image is run length encoded (RLE).
   
     If the image is not run length encoded, this is the structure:
   
           The Header
           The Image Data
   
     If the image is run length encoded, this is the structure:
   
           The Header
           The Offset Tables 
           The Image Data
   
   
   The Header
   
     The header consists of the following:
   
        Size  | Type   | Name      | Description   
   
      2 bytes | short  | MAGIC     | IRIS image file magic number
      1 byte  | char   | STORAGE   | Storage format
      1 byte  | char   | BPC       | Number of bytes per pixel channel 
      2 bytes | ushort | DIMENSION | Number of dimensions
      2 bytes | ushort | XSIZE     | X size in pixels 
      2 bytes | ushort | YSIZE     | Y size in pixels 
      2 bytes | ushort | ZSIZE     | Number of channels
      4 bytes | long   | PIXMIN    | Minimum pixel value
      4 bytes | long   | PIXMAX    | Maximum pixel value
      4 bytes | char   | DUMMY     | Ignored
     80 bytes | char   | IMAGENAME | Image name
      4 bytes | long   | COLORMAP  | Colormap ID
    404 bytes | char   | DUMMY     | Ignored
   
   
     Here is a description of each field in the image file header:
   
           MAGIC - This is the decimal value 474 saved as a short. This
           identifies the file as an SGI image file.
   
           STORAGE - specifies whether the image is stored using run
           length encoding (RLE) or not (VERBATIM).   If RLE is used, the value 
        of this byte will be 1.  Otherwise the value of this byte will be 0.
           The only allowed values for this field are 0 or 1.
   
           BPC - describes the precision that is used to store each
           channel of an image.  This is the number of bytes per pixel
           component.  The majority of SGI image files use 1 byte per 
           pixel component, giving 256 levels.  Some SGI image files use 
           2 bytes per component.  The only allowed values for this field 
           are 1 or 2.
   
           DIMENSION - described the number of dimensions in the data stored
           in the image file.  The only allowed values are 1, 2, or 3.  If
           this value is 1, the image file consists of only 1 channel and 
           only 1 scanline (row).  The length of this scanline is given by the 
           value of XSIZE below.  If this value is 2, the file consists of a 
           single channel with a number of scanlines. The width and height
           of the image are given by the values of XSIZE and YSIZE below.
           If this value is 3, the file consists of a number of channels.
           The width and height of the image are given by the values of 
           XSIZE and YSIZE below.  The number of channels is given by the 
           value of ZSIZE below.  
   
           XSIZE - The width of the image in pixels
   
           YSIZE - The height of the image in pixels
   
           ZSIZE - The number of channels in the image.  B/W (greyscale) images 
           are stored as 2 dimensional images with a ZSIZE or 1.  RGB color 
           images are stored as 3 dimensional images with a ZSIZE of 3.  An RGB 
           image with an ALPHA channel is stored as a 3 dimensional image with 
           a ZSIZE of 4.  There are no inherent limitations in the SGI image 
           file format that would preclude the creation of image files with more 
           than 4 channels.
   
           PINMIN - The minimum pixel value in the image.  The value of
           0 may be used if no pixel has a value that is smaller than 0.
   
           PINMAX - The maximum pixel value in the image.  The value of
           255 may be used if no pixel has a value that is greater than 255.
           This is the value that is considered to be full brightness in 
           the image.  
   
           DUMMY - This 4 bytes of data should be set to 0. 
   
           IMAGENAME - An null terminated ascii string of up to 79 characters 
           terminated by a null may be included here.  This is not commonly
           used.
   
           COLORMAP - This controls how the pixel values in the file should be
           interpreted.  It can have one of these four values:
   
               0:  NORMAL - The data in the channels represent B/W values
                   for images with 1 channel, RGB values for images with 3
                   channels, and RGBA values for images with 4 channels.
                   Almost all the SGI image files are of this type. 
   
               1:  DITHERED - The image will have only 1 channel of data.
                   For each pixel, RGB data is packed into one 8 bit value.
                   3 bits are used for red and green, while blue uses 2 bits.
                   Red data is found in bits[2..0], green data in bits[5..3],
                   and blue data in bits[7..6].  This format is obsolete.
   
               2:  SCREEN - The image will have only 1 channel of data.
                   This format was used to store color-indexed pixels.
                   To convert the pixel values into RGB values a colormap
                   must be used.  The appropriate color map varies from
                   image to image.  This format is obsolete.
   
               3:  COLORMAP - The image is used to store a color map from
                   an SGI machine.  In this case the image is not displayable
                   in the conventional sense.
   
           DUMMY - This 404 bytes of data should be set to 0. This makes the
           header exactly 512 bytes. 
   
   
   The Image Data (if not RLE)
   
     If the image is stored verbatim (without RLE), then image data directly
     follows the 512 byte header.  The data for each scanline of the first
     channel is written first.  If the image has more than 1 channel, all
     the data for the first channel is written, followed by the remaining
     channels.  If the BPC value is 1, then each scanline is written as XSIZE
     bytes.  If the BPC value is 2, then each scanline is written as XSIZE 
     shorts.  These shorts are stored in the byte order described above.
   
   
   The Offset Tables (if RLE)
   
     If the image is stored using run length encoding, offset tables
     follow the header that describe what the file offsets are to the 
     RLE for each scanline.  This information only applies if the value 
     for STORAGE above is 1.
   
             Size  | Type   | Name      | Description   
   
      tablen longs | long   | STARTTAB  | Start table
      tablen longs | long   | LENGTHTAB | Length table
   
     One entry in each table is needed for each scanline of RLE data.  The 
     total number of scanlines in the image (tablen) is determined by the
     product of the YSIZE and ZSIZE.  There are two tables of longs that 
     are written. Each consists of tablen longs of data.  The first
     table has the file offsets to the RLE data for each scanline in the
     image.  In a file with more than 1 channel (ZSIZE > 1) this table first 
     has all the offsets for the scanlines in the first channel, followed
     be offsets for the scanlines in the second channel, etc.  The second 
     table has the RLE data length for each scanline in the image.  In a 
     file with more than 1 channel (ZSIZE > 1) this table first has all the 
     RLE data lengths for the scanlines in the first channel, followed
     be RLE data lengths for the scanlines in the second channel, etc.
   
     To find the the file offset, and the number of bytes in the RLE data 
     for a particular scanline, these two arrays may be read in and indexed as
     follows: 
   
           To read in the tables:
   
               unsigned long *starttab, *lengthtab;
   
               tablen = YSIZE*ZSIZE*sizeof(long);
               starttab = (unsigned long *)mymalloc(tablen);
               lengthtab = (unsigned long *)mymalloc(tablen);
               fseek(inf,512,SEEK_SET);
               readlongtab(inf,starttab);
               readlongtab(ing,lengthtab);
   
   
           To find the file offset and RLE data length for a scanline:
   
               rowno is an integer in the range 0 to YSIZE-1
               channo is an integer in the range 0 to ZSIZE-1
   
               rleoffset = starttab[rowno+channo*YSIZE]
               rlelength = lengthtab[rowno+channo*YSIZE]
     
     It is possible for two identical rows (scanlines) to share compressed 
     data.  A completely white image could be written as a single compressed 
     row and having all table entries point to that row.  Another little hack 
     that should work is if you are writing out a RGB RLE file, and a 
     particular scanline is achromatic (greyscale), you could just make the 
     r, g and b rows point to the same data!!
   
   The Image Data (if RLE)
   
     This information only applies if the value for STORAGE above is 1.  If
     the image is stored using run length encoding, the image data follows
     the offset tables above.  The RLE data is not in any particular order.
     The offset tables above are used to locate the rle data for any scanline.
   
     The RLE data must be read in from the file and expanded into pixel 
     data in the following manner:
   
     If BPC is 1, then there is one byte per pixel.  In this case the 
     RLE data should be read into an array of chars.  To expand
     data, the low order seven bits of the first byte: bits[6..0]
     are used to form a count.  If the high order bit of the first
     byte is 1: bit[7], then the count is used to specify how many
     bytes to copy from the RLE data buffer to the destination.
     Otherwise, if the high order bit of the first byte is 0: bit[7],
     then the count is used to specify how many times to repeat the 
     value of the following byte, in the destination.  This process
     continues until a count of 0 is found.  This should decompress
     exactly XSIZE pixels.  
   
           Here is example code to decompress a scanline:
   
               expandrow(optr,iptr,z)
               unsigned char *optr, *iptr;
               int z;
               {
                   unsigned char pixel, count;
               
                   optr += z;
                   while(1) {
                       pixel = *iptr++;
                       if ( !(count = (pixel & 0x7f)) )
                           return;
                       if(pixel & 0x80) {
                           while(count--) {
                               *optr = *iptr++;
                               optr+=4;
                           }
                       } else {
                           pixel = *iptr++;
                           while(count--) {
                               *optr = pixel;
                               optr+=4;
                           }
                       }
                   }
               }
   
     If BPC is 2, there is one short (2 bytes) per pixel.  In this 
     case the RLE data should be read into an array of shorts.  To 
     expand data, the low order seven bits of the first short: bits[6..0]
     are used to form a count.  If bit[7] of the first short is 1, then 
     the count is used to specify how many shorts to copy from the RLE 
     data buffer to the destination.  Otherwise, if bit[7] of the first 
     short is 0, then the count is used to specify how many times to 
     repeat the value of the following short, in the destination.  This 
     process proceeds until a count of 0 is found.  This should decompress
     exactly XSIZE pixels.  Note that the byte order of short data in
     the input file should be used, as described above.
   
   
   Implementation notes
   
     Implementation of both RLE and VERBATIM format for images with
     BPC of 1 is required since the great majority of SGI images are in
     this format.  Support for images with a 2 BPC is encouraged.
   
     If the ZSIZE of an image is 1, it is assumed to represent B/W
     values.  If the ZSIZE is 3, it is assumed to represent RGB data,
     and if ZSIZE is 4, it is assumed to contain RGB data with alpha.
   
     The origin for all SGI images is the lower left hand corner.  The
     first scanline (row 0) is always the bottom row of the image.   
   
   Naming Conventions
   
     On SGI systems, SGI image files end with the extension .bw if
     they are B/W images, they end in .rgb if they contain RGB image
     data, and end in .rgba if they are RGB images with alpha channel.
   
     Sometimes the .sgi extension is used as well.
   
   An example
   
  This program will write out a valid B/W SGI image file:
   
  #include "stdio.h"
   
  #define IXSIZE      (23)
  #define IYSIZE      (15)
   
  putbyte(outf,val)
  FILE *outf;
  unsigned char val;
  {
    unsigned char buf[1];
   
    buf[0] = val;
    fwrite(buf,1,1,outf);
  }
   
  putshort(outf,val)
  FILE *outf;
  unsigned short val;
  {
    unsigned char buf[2];
   
    buf[0] = (val>>8);
    buf[1] = (val>>0);
    fwrite(buf,2,1,outf);
  }
   
  static int putlong(outf,val)
  FILE *outf;
  unsigned long val;
  {
    unsigned char buf[4];
   
    buf[0] = (val>>24);
    buf[1] = (val>>16);
    buf[2] = (val>>8);
    buf[3] = (val>>0);
    return fwrite(buf,4,1,outf);
  }
   
  main()
  {
    FILE *of;
    char iname[80];
    unsigned char outbuf[IXSIZE];
    int i, x, y;
   
    of = fopen("example.rgb","w");
    if(!of) {
        fprintf(stderr,"sgiimage: can't open output file\n");
        exit(1);
    }
    putshort(of,474);       /* MAGIC                       */
    putbyte(of,0);          /* STORAGE is VERBATIM         */
    putbyte(of,1);          /* BPC is 1                    */
    putshort(of,2);         /* DIMENSION is 2              */
    putshort(of,IXSIZE);    /* XSIZE                       */
    putshort(of,IYSIZE);    /* YSIZE                       */
    putshort(of,1);         /* ZSIZE                       */
    putlong(of,0);          /* PIXMIN is 0                 */
    putlong(of,255);        /* PIXMAX is 255               */
    for(i=0; i<4; i++)      /* DUMMY 4 bytes       */
        putbyte(of,0);
    strcpy(iname,"No Name");
    fwrite(iname,80,1,of);  /* IMAGENAME           */
    putlong(of,0);          /* COLORMAP is 0       */
    for(i=0; i<404; i++)    /* DUMMY 404 bytes     */
        putbyte(of,0);
   
    for(y=0; y<IYSIZE; y++) {
        for(x=0; x<IXSIZE; x++) 
            outbuf[x] = (255*x)/(IXSIZE-1);
        fwrite(outbuf,IXSIZE,1,of);
    }
    fclose(of);
  }
  } ;# end comment (SGI 0.97 spec)

schlenk This could fit into tcllib like jpeg and other packages to read image metadata.