Home Manual Packages Global Index Keywords Quick Reference |
    /*
       FITS.I
       Routines to manipulate FITS files (IAU astronomical data format).
       Anonymous FTP site:    nssdc.gsfc.nasa.gov:/pub/fits

       $Id$
     */
    /*    Copyright (c) 1996.  The Regents of the University of California.
                        All rights reserved.  */

    /*----------------------------------------------------------------------
     * @(#) fits.i: manipulation of FITS files for Yorick by Eric THIEBAUT.
     */

    require, "string.i";

    /*----------------------------------------------------------------------*/

    local fits_max_naxis ;

    /* DOCUMENT fits_max_naxis -- maximum number of allowed axis in FITS
    	Its value should *NOT* be modified during a Yorick session.
    */

    fits_max_naxis= 9;	/* if you need more axis, change this value in this
    			 * file and source it again. */

    struct FitsHeader {
      int		bitpix;		//   8	pixel values are unsigned bytes
    				//  16	pixel values are signed 2-byte integers
    				//  32	pixel values are signed 4-byte integers
    				// -32	pixel values are 4-byte floating points
    				// -64	pixel values are 8-byte floating points
    				// -128	pixel values are 2x8-byte complex
      int		naxis;		// number of axis

      int		axis(fits_max_naxis);	// number of pixel along axis No. n
      // Pixel intensity / brighness:
      double	bscale;		// pixelValue = BZERO+BSCALE*fileValue
      double	bzero;		// pixelValue = BZERO+BSCALE*fileValue
      string	bunit;		// brightness unit
      double	datamax;	// maximum data value in the file
      double	datamin;	// minimum data value in the file
      // Miscellaneous information:
      string	object;		// image name
      string	date;		// date of file creation (dd/mm/yy)
      string	date_obs;	// date of data acquisition (dd/mm/yy)
      string	origin;		// institution
      string	instrume;	// data acquisition instrument
      string	telescop;	// data acquisition telescope
      string	observer;	// observer name/identification
      string	history;	// newline separated history lines
      string	comment;	// newline separated comment lines
      // coordinate system for each axis:

      double	epoch;		// epoch of coordinate system (year)

      double	crval(fits_max_naxis);	// coord = CRVAL+(pixel-CRPIX)*CDELT

      double	crpix(fits_max_naxis);	// coord = CRVAL+(pixel-CRPIX)*CDELT

      double	cdelt(fits_max_naxis);	// coord = CRVAL+(pixel-CRPIX)*CDELT

      string	ctype(fits_max_naxis);	// type of physical coordinate

      double	crota(fits_max_naxis);	// rotation angle of axis No. #
    }

    /*----------------------------------------------------------------------*/

    func fitsHeader (&header)

    /* DOCUMENT header= fitsHeader();
                fitsHeader, header;

         creates a structure FitsHeader with defaults.  Equivalent to:

             header= FitsHeader(bscale= 1., bzero= 0.);

       SEE ALSO: fitsRead, fitsWrite.
    */
    {

      return (header= FitsHeader(bscale= 1., bzero= 0.));
    }

    func fitsFixHeader (&header)

    /* DOCUMENT fitsFixHeader, header
    	check consistency of FITS header.
       SEE ALSO: fitsRead, fitsWrite.
    */
    {

      if (structof(header) != FitsHeader)

        error, "bad type for HEADER (not FitsHeader)";
      if (header.datamin > header.datamax)

        error, "DATAMIN greater than DATAMAX in FITS header!";
      if (header.bscale == 0. && header.bzero != 0.) {

        write, "WARNING: BZERO ignored because BSCALE is zero";
        header.bzero= 0.;
      }
    }

    func fitsAddComment (&header, str)

    /* DOCUMENT fitsAddComment, header, string

    	Add STRING as a comment in HEADER (a FitsHeader structure). If
    	STRING is too long, it is broken in pieces.
       SEE ALSO: fitsRead, fitsWrite.
    */
    {

      //if (structof(header) != FitsHeader)

      //	error, "bad type for HEADER (not FitsHeader)";

      str= strtrim(str);	// remove trailing/leading blanks
      while (strlen(str)) {

        header.comment+= strpart(str, :69)+"\n";

        str= strtrim(strpart(str, 70:), 1);
      }
    }

    func fitsAddHistory (&header, str, stamp=)

    /* DOCUMENT fitsAddHistory, header, string

    	Add STRING as an history card in HEADER (a FitsHeader structure).
    	If STRING is too long, it is broken in pieces.
    	Keyword STAMP may be used with a zero value to avoid the
    	additional time stamp (default is to add a time stamp).
       SEE ALSO: fitsRead, fitsWrite.
    */
    {

      //if (structof(header) != FitsHeader)

      //	error, "bad type for HEADER (not FitsHeader)";

      str= strtrim(str);	// remove trailing/leading blanks
      if (is_void(stamp))
        stamp= 1N;	// default is to add a time stamp
      if (stamp) {

        str= getdate()+" "+gettime()+": "+str;
        indent= "                   ";
      } else {
        indent= "";
      }
      len= 69 - strlen(indent);

      header.history+= strpart(str, :69)+"\n";

      str= strtrim(strpart(str, 70:), 1);
      while (strlen(str)) {

        header.history+= indent + strpart(str, :len) + "\n";

        str= strtrim(strpart(str, len+1:), 1);
      }
    }

    /*----------------------------------------------------------------------*/

    func fitsRescale (data, bitpix, &bscale, &bzero, data_min=, data_max=)

    /* DOCUMENT rescaled_data= fitsRescale(data, bitpix, bscale, bzero)

    	Linearly rescale the values of input array DATA to fit into
    	integers with BITPIX bits per value (i.e., `char', `short' or
    	`long' for BITPIX being 8, 16 and 32 respectively).

    	Arguments BSCALE and BZERO are optional and purely outputs passed
    	by address.  Their value will be set so that:
    		DATA(i) = BZERO + BSCALE * RESCALED_DATA(i)
    	where the equality may not be exact due to rounding errors.  The
    	difference is however the smallest possible, i.e., less than
    	BSCALE / 2.

    	Keywords DATA_MIN and DATA_MAX may be used to supply the maximum
    	and minimum data values or to set brightness cutoffs.

       SEE ALSO: fitsRead, fitsWrite.
    */
    {
      if (bitpix == 8) {
        // assume ``char'' is 8 bits unsigned
        file_type= char;
        file_unsigned= 1N;
      } else if (bitpix == 16) {
        // assume ``short'' is 16 bits signed
        file_type= short;
        file_unsigned= 0N;
      } else if (bitpix == 32) {
        // assume ``long'' is 32 bits signed
        file_type= long;
        file_unsigned= 0N;
      } else {

        error, "bad BITPIX (should be 8, 16 or 32)";
      }

      data_min= double((is_void(data_min)? min(data) : data_min));

      data_max= double((is_void(data_max)? max(data) : data_max));

      if (data_max < data_min) error, "bad DATA_MAX and DATA_MIN";

      if (file_unsigned) {
        file_min= 0.;
        file_max= 2.^bitpix - 1.;
      } else {
        file_min= -2.^(bitpix - 1);
        file_max=  2.^(bitpix - 1) - 1.;
      }

      if (data_max == data_min) {
        bzero= double(data_min);

        return array(file_type(0), dimsof(data));
      }

      bscale= (data_max - data_min) / (file_max - file_min);
      bzero= data_min - bscale * file_min;

      return file_type(min(file_max, max(file_min, (data - bzero) / bscale)));
    }

    /*----------------------------------------------------------------------*/

    func fitsWrite (name, data, header, rescale=, pack=)

    /* DOCUMENT fitsWrite, filename, data, header
    	Write DATA in file FILENAME using FITS format.  If present, the

    	information of the optional argument HEADER  (a FitsHeader structure)

    	will be used to write the FITS file header.

    	Keyword PACK, if non-nil and non-zero, indicates that axis whith
    	unit dimension should be ignored.  The default is to ignore only
    	zero length axis.

    	Keyword RESCALE, if non-nil and zero, indicates that input data
    	values should not be rescaled into integers according to FITS
    	standard.  The default, is to recode doubles as 32 bit integers,
    	floats as 16 bit integers and integers into smallest integers

    	possible without loss of dynamic range (e.g., an array of long
    	integers ranging from -31 to +130 will be recoded as chars).

      SEE ALSO: fitsRead, fitsRescale, fitsAddHistory, fitsAddComment.
    */
    {
      local bitpix, bzero, bscale;

      if (is_void(data)) error, "empty DATA array!!!";

      if (dimsof(data)(1) > fits_max_naxis) {

        write, swrite(format="*** Currently, number of dimensions for "+
    		  "FITS cannot exceed %d\n*** If you really need "+
    		  "more dimensions, you'll have to modify the\n"+

    		  "*** source file...", int(fits_max_naxis));
        error, "too many dimensions";
      }

      if (is_void(header)) {

        fitsHeader, header;	// create a FITS header with defaults
      } else {

        fitsFixHeader, header;	// just check header
      }
      if (strlen(header.date) == 0) {

        header.date= getdate()+" "+gettime();
      }

      // rescale brightness to fit standards
      if (is_void(rescale) || rescale) {
        if (header.bscale != 1. || header.bzero != 0.)

          write, "WARNING: initial [BSCALE,BZERO] not equal to [1,0]";

        if (structof(data) == float || structof(data) == double) {

          if (structof(data) == float) {
    	bitpix= 16;	// float  -> short
          } else {
    	bitpix= 32;	// double -> long
          }

          data= fitsRescale(data, bitpix, bscale, bzero);
          header.bitpix= bitpix;
          header.bzero+= header.bscale * bzero;
          header.bscale*= bscale;

        } else if (structof(data) == short ||
    	       structof(data) == int ||
    	       structof(data) == long) {

          // Integers: check if data fit into smaller data type

          if (structof(data) == short) bitpix= 16;

          else if (structof(data) == int) bitpix= 32;
          else bitpix= 32;
          data_max= double(max(data));
          data_min= double(min(data));
          data_range= 1.+data_max-data_min;
          if (data_range<=256.) {
    	file_type= char;
    	bzero= double(data_min);
    	bitpix= 8;
          } else if (bitpix==32 && data_range<=65536.) {
    	file_type= short;
    	bzero= data_min + 32768.;
    	bitpix= 16;
          } else {
    	file_type= bitpix==32? long : short;
    	bzero= data_min + 2.^(bitpix - 1);
          }
          data= file_type(data - bzero);
          header.bitpix= bitpix;
          header.bzero+= header.bscale * bzero;

        } else if (structof(data) == char) {
          header.bitpix= 8;
        } else {

          error, "data type not supported";
        }

      } else {
        if (header.bscale != 1. || header.bzero != 0.)

          write, "WARNING: no rescaling, but non-unit BSCALE or non-zero BZERO";
        if (structof(data) == char) {
          header.bitpix= 8;

        } else if (structof(data) == short) {
          header.bitpix= 16;

        } else if (structof(data) == int) {
          data= long(data);  // for consistency across all platforms
          header.bitpix= 32;

        } else if (structof(data) == long) {
          header.bitpix= 32;

        } else if (structof(data) == float) {
          header.bitpix= -32;

        } else if (structof(data) == double) {
          header.bitpix= -64;

        } else if (structof(data) == complex) {

          write, "WARNING: using BITPIX=-128 is not standard FITS";
          header.bitpix= -128;
        } else {

          error, "bad data type for DATA";
        }
      }

      // compute dimensions list
      header.axis(*)= 0;
      dims= dimsof(data);
      if (dims(1) == 0) {
        // data is a scalar!
        header.naxis= 1;
        header.axis(1)= 1;
      } else {
        // there is at least one dimension
        dims= dims(2:);
        if (is_void(pack) || pack) {
          i= where(dims > 1);
          if (numberof(i)) {
    	dims= dims(i);
          } else {
    	dims= [1];
          }
        }
        header.naxis= numberof(dims);
        header.axis(1:header.naxis)= dims;
      }

      // write data file
      local address;
      if (open(name+"L", "", 1)) {

        write, format="%s", "Remove existing file "+name+"L (y or n)? ";

        response= strtok(rdline(prompt=string(0)))(1);
        if (response=="y" || response=="yes") {
          remove, name+"L";
        } else {

          write, "No file written or deleted.";
          return;
        }
      }
      f= open(name, "wb");
      sun_primitives, f;
      address= 0L;

      _fitsPutLogical, f, address, "SIMPLE", 1;

      _fitsPutInteger, f, address, "BITPIX", header.bitpix;

      _fitsPutInteger, f, address, "NAXIS", header.naxis;
      for (i=1; i<=header.naxis; i++) {

        _fitsPutInteger, f, address, swrite(format="NAXIS%d", i), header.axis(i);
      }
      if (header.bscale != 1.0 || header.bzero != 0.) {

        _fitsPutReal, f, address, "BSCALE", header.bscale;

        _fitsPutReal, f, address, "BZERO", header.bzero;
      }
      if (strlen(header.bunit))

        _fitsPutString, f, address, "BUNIT", header.bunit;
      if (header.datamax || header.datamin) {

        _fitsPutReal, f, address, "DATAMAX", header.datamax;

        _fitsPutReal, f, address, "DATAMIN", header.datamin;
      }
      if (strlen(header.object))

        _fitsPutString, f, address, "OBJECT", header.object;
      if (strlen(header.date))

        _fitsPutString, f, address, "DATE", header.date;
      if (strlen(header.date_obs))

        _fitsPutString, f, address, "DATE-OBS", header.date_obs;
      if (strlen(header.origin))

        _fitsPutString, f, address, "ORIGIN", header.origin;
      if (strlen(header.instrume))

        _fitsPutString, f, address, "INSTRUME", header.instrume;
      if (strlen(header.telescop))

        _fitsPutString, f, address, "TELESCOP", header.telescop;
      if (strlen(header.observer))

        _fitsPutString, f, address, "OBSERVER", header.observer;
      if (header.epoch)

        _fitsPutReal, f, address, "EPOCH", header.epoch;
      for (i=1; i<=header.naxis; i++) {
        if (strlen(header.ctype(i)))

          _fitsPutString, f, address, swrite(format="CTYPE%d", i), header.ctype(i);
        if (header.cdelt(i)) {

          _fitsPutReal, f, address, swrite(format="CRVAL%d", i), header.crval(i);

          _fitsPutReal, f, address, swrite(format="CRPIX%d", i), header.crpix(i);

          _fitsPutReal, f, address, swrite(format="CDELT%d", i), header.cdelt(i);

          _fitsPutReal, f, address, swrite(format="CROTA%d", i), header.crota(i);
        }
      }
      if (strlen(header.comment)) {

        comment= strtok(header.comment, "\n");
        while (comment(1)) {

          _fitsPutComment, f, address, "COMMENT", comment(1);

          comment= strtok(comment(2), "\n");
        }
      }
      if (strlen(header.history)) {

        history= strtok(header.history, "\n");
        while (history(1)) {

          _fitsPutComment, f, address, "HISTORY", history(1);

          history= strtok(history(2), "\n");
        }
      }
      _fitsPutEnd,f, address;
      _write, f, address, data;
      close, f;

      // Yorick uses a Contents Log file, which should be removed...
      if (open(name+"L", "", 1))
        remove, name+"L";
    }

    /*----------------------------------------------------------------------*/

    func fitsRead (name, &header, which=, pack=, rescale=)

    /* DOCUMENT a= fitsRead(filename, header)
    	returns the data of the FITS file FILENAME.  If present, the
    	optional argument HEADER will be used to store the contents of

    	the FITS header file (a FitsHeader structure).

    	Keyword WHICH may be used to indicate which sub-array should be

    	returned.  For instance, if the array DATA with dimensions

    	(235,453,7) is stored in the FITS file "data.fits", the sub-array
    	DATA(,,4) can be read by:

    		SUB_DATA= fitsRead("data.fits", which= 4);

    	Keyword PACK, if non-nil and non-zero, indicates that axis whith
    	unit dimension should be ignored.  The default is to ignore only
    	zero length axis.

    	Keyword RESCALE, if non-nil and zero, indicates that read data
    	values should not be rescaled according to FITS keywords
    	BSCALE and BZERO.  The default is to rescale data values if
    	BSCALE is not 1. or BZERO is not 0.

      SEE ALSO: fitsWrite, fitsRescale.
    */
    {
      local axis;

      if (is_void(pack))

        pack= 1;	// default is to ignore zero *AND* unit dimensions
      if (is_void(rescale))
        rescale= 1;	// default is to rescale brightness

      fitsHeader, header;		// Create FITS header with defaults
      file= open(name, "rb");
      sun_primitives, file;
      buffer= array(char, 80, 36);
      buffer_size= 80*36;
      end_not_found= 1;
      n= 1;
      address= 0;
      do {

        if (_read(file, address, buffer) != buffer_size)

          error, "cannot read header";
        address+= buffer_size;

        for (i=1; i<=dimsof(buffer)(3) && end_not_found; i++, n++) {
          line= string(&buffer(,i));

          keyword= strtoupper(strtrim(strpart(line, 1:8), 2, blank=" "));

          value= strtrim(strpart(line, 9:80), 2);
          if (n==1) {

    	if (keyword != "SIMPLE" || !_fitsGetLogical(keyword, value))
    	  error, "not a standard FITS file";
    	continue;
          }
          if (n==2) {
    	if (keyword != "BITPIX" ||

    	    noneof(((header.bitpix= _fitsGetInteger(keyword, value)) ==
    		    [8, 16, 32, 64, -32, -64, -128])))

    	  error, "bad BITPIX or no BITPIX at line 2";
    	continue;
          }
          if (n==3) {
    	if (keyword != "NAXIS" ||

    	    (header.naxis= _fitsGetInteger(keyword, value)) < 1)

    	  error, "bad NAXIS or no NAXIS at line 3";

    	if (header.naxis > fits_max_naxis) {

    	  write, swrite(format="*** Currently, number of dimensions for "+
    			"FITS cannot exceed %d\n*** If you really need "+
    			"more dimensions, you'll have to modify the\n"+

    			"*** source file...", int(fits_max_naxis));

    	  error, swrite(format="NAXIS=%d too large", header.naxis);
    	}
    	naxis= header.naxis;
    	continue;
          }
          if (naxis) {
    	k= n-3;
    	s= swrite(format="NAXIS%d", k);
    	if (keyword == s) {

    	  if ((header.axis(k)= _fitsGetInteger(keyword, value)) < 0)
    	    error, "bad "+s;
    	  continue;
    	}
    	naxis= 0;	// no more NAXIS to read
          }
          if (keyword == "END") {
    	end_not_found=0;
    	break;
          }
          if (keyword == "") {
    	continue;
          }
          if (keyword == "COMMENT") {
    	value= strtrim(value);
    	if (strlen(value))
    	  header.comment+= value+"\n";
    	continue;
          }
          if (keyword == "HISTORY") {
    	value= strtrim(value);
    	if (strlen(value))
    	  header.history+= value+"\n";
    	continue;
          }
          if (keyword == "BSCALE") {

    	header.bscale= _fitsGetReal(keyword, value);
    	continue;
          }
          if (keyword == "BZERO") {

    	header.bzero= _fitsGetReal(keyword, value);
    	continue;
          }
          if (keyword == "BUNIT") {

    	header.bunit= _fitsGetString(keyword, value);
    	continue;
          }
          if (keyword == "DATAMAX") {

    	header.datamax= _fitsGetReal(keyword, value);
    	continue;
          }
          if (keyword == "DATAMIN") {

    	header.datamin= _fitsGetReal(keyword, value);
    	continue;
          }
          if (keyword == "EPOCH") {

    	header.epoch= _fitsGetReal(keyword, value);
    	continue;
          }
          if (keyword == "OBJECT") {

    	header.object= _fitsGetString(keyword, value);
    	continue;
          }
          if (keyword == "DATE") {

    	header.date= _fitsGetString(keyword, value);
    	continue;
          }
          if (keyword == "DATE-OBS") {

    	header.date_obs= _fitsGetString(keyword, value);
    	continue;
          }
          if (keyword == "ORIGIN") {

    	header.origin= _fitsGetString(keyword, value);
    	continue;
          }
          if (keyword == "INSTRUME") {

    	header.instrume= _fitsGetString(keyword, value);
    	continue;
          }
          if (keyword == "TELESCOP") {

    	header.telescop= _fitsGetString(keyword, value);
    	continue;
          }
          if (keyword == "OBSERVER") {

    	header.observer= _fitsGetString(keyword, value);
    	continue;
          }

          if (strpart(keyword, 1:1) == "C") {
    	axis=long();

    	if (sread(keyword, format="CRVAL%d", axis)) {
    	  if (axis < 1 || axis > header.naxis)
    	    error, "bad "+keyword;

    	  header.crval(axis)= _fitsGetReal(keyword, value);
    	  continue;
    	}

    	if (sread(keyword, format="CRPIX%d", axis)) {
    	  if (axis < 1 || axis > header.naxis)
    	    error, "bad "+keyword;

    	  header.crpix(axis)= _fitsGetReal(keyword, value);
    	  continue;
    	}

    	if (sread(keyword, format="CDELT%d", axis)) {
    	  if (axis < 1 || axis > header.naxis)
    	    error, "bad "+keyword;

    	  header.cdelt(axis)= _fitsGetReal(keyword, value);
    	  continue;
    	}

    	if (sread(keyword, format="CROTA%d", axis)) {
    	  if (axis < 1 || axis > header.naxis)
    	    error, "bad "+keyword;

    	  header.crota(axis)= _fitsGetReal(keyword, value);
    	  continue;
    	}

    	if (sread(keyword, format="CTYPE%d", axis)) {
    	  if (axis < 1 || axis > header.naxis)
    	    error, "bad "+keyword;

    	  header.ctype(axis)= _fitsGetString(keyword, value);
    	  continue;
    	}
          }
          if (keyword == "BLANK") {
    	if (strpart(value, 1:1) == "=") {

    	  header.blank= _fitsGetInteger(keyword, value);
    	} else {
    	  value= strtrim(value);
    	  if (strlen(value))

    	    write, "WARNING: discarding BLANK comment ``"+value+"''";
    	}
    	continue;
          }

          write, "WARNING: unknown keyword ``"+keyword+"''";
        }
      } while (end_not_found);

      /*

       * Get dimensions (ignore zero and unit dimensions, keep

       * unit dimensions if keyword `pack' is zero):
       */
      dims= array(long, 1+header.naxis);
      dims(1)=  2;	// just a dummy value!
      dims(2:)= header.axis(1:header.naxis);
      i= where(dims > (pack ? 1 : 0));
      dims(1)=  numberof(i) - 1;
      if (dims(1) <= 0) {
        write, "WARNING empty data file";
        close, file;
        return [];
      }
      dims= dims(i);

      if (is_void(which)) {
        which=0;
      } else {

        if (numberof(which) != 1 || which != long(which))

          error, "WHICH must be a scalar integer";
        last= dims(0);
        if (which <= 0)
          which+= last;
        if (which > last || which < 1)
          error, "WHICH out of range";
        dims= dims(:-1);
        dims(1)-= 1;
      }

      if (header.bitpix == 8) {
        data= array(char, dims);
        data_size= 1;
      } else if (header.bitpix == 16) {
        data= array(short, dims);
        data_size= 2;
      } else if (header.bitpix == 32) {
        data= array(long, dims);
        data_size= 4;
      } else if (header.bitpix == -32) {
        data= array(float, dims);
        data_size= 4;
      } else if (header.bitpix == -64) {
        data= array(double, dims);
        data_size= 8;
      } else if (header.bitpix == -128) {

        write, "WARNING: using BITPIX=-128 is not standard FITS";
        data= array(complex, dims);
        data_size= 16;
      } else {

        error, "congratulations: you have found a BUG!";
      }
      if (which > 1)

        address+= (which - 1) * numberof(data) * data_size;

      if (_read(file, address, data) != numberof(data))

        error, "cannot read data";
      close, file;

      fitsFixHeader, header;

      /*
       * Rescale pixel values
       */
      if (rescale && (header.bscale != 1. || header.bzero != 0.)) {
        if (abs(header.bitpix) < 32) {
          data=  float(header.bzero + header.bscale * data);
          header.bitpix= -32;	// data is now 4-byte float
        } else {
          data= header.bzero + header.bscale * data;
          header.bitpix= -64;	// data is now 8-byte double
        }
        header.bscale= 1.;
        header.bzero= 0.;
      }

      return data;
    }

    /*----------------------------------------------------------------------*/

    func _fitsGetLogical (keyword, value)
    {
      x= string();
      if (strpart(value, 1:1) == "=" &&

          sread(value, format="=%s", x) == 1 && strlen(x) == 1) {
        if (x == "T" || x == "t")
          return 1;
        if (x == "F" || x == "f")
          return 0;
      }

      error, "bad logical value for "+keyword;
    }

    func _fitsGetInteger (keyword, value)
    {
      x= long();

      if (strpart(value, 1:1) == "=" && sread(value, format="=%d", x) == 1)
        return x;

      error, "bad integer value for "+keyword;
    }

    func _fitsGetReal (keyword, value)
    {
      x= double();

      if (strpart(value, 1:1) == "=" && sread(value, format="=%f", x) == 1)
        return x;

      error, "bad real value for "+keyword;
    }

    func _fitsGetString (keyword, value)
    {
      if (strpart(value, 1:1) == "=") {
        /* note that this does not allow for a / delimited comment field... */

        x= strtrim(strpart(value, 2:));
        if (strpart(x, 1:1) == "'") {
        // assume that the string is quoted

        if ((i= strchr(x, '\'', last=1)) > 1)

          return strtrim(strpart(x, 2:i-1));
        } else {
          return x;
        }
      }
      //error, "bad value for "+keyword;

      write, "WARNING: bad string value for "+keyword;
      return "";
    }

    /*----------------------------------------------------------------------*/

    func _fitsPutValue (stream, &address, format, keyword, value, comment)
    {
      if (is_void(address)) {
        address= 0L;

      } else if (structof(address) != long) {
        write, "ADDRESS=",address;
        error, "bad ADDRESS";
      }

      comment= is_void(comment)? "" : strpart(comment, 1:46);

      line= swrite(format=format, keyword, value, comment);
      if (strlen(line) != 80) {
        write, "BAD LINE: "+line;
        error, "bug...";
      }
      line= *pointer(line);

      _write, stream, address, line(1:80);
      address+= 80;
    }

    func _fitsPutInteger (stream, &address, keyword, value, comment)
    {

      _fitsPutValue, stream, address, "%-8s= %20ld / %-46s\n", keyword,
        long(value), comment;
    }

    func _fitsPutReal (stream, &address, keyword, value, comment)
    {

      _fitsPutValue, stream, address, "%-8s= %#20.13G / %-46s\n", keyword,
        double(value), comment;
    }

    func _fitsPutString (stream, &address, keyword, value, comment)
    {

      _fitsPutValue, stream, address, "%-8s= '%-18s' / %-46s\n", keyword,
        strpart(value, 1:18), comment;
    }

    func _fitsPutLogical (stream, &address, keyword, value, comment)
    {

      _fitsPutValue, stream, address, "%-8s= %20s / %-46s\n", keyword,
        (value? "T" : "F"), comment;
    }

    func _fitsPutComment (stream, &address, keyword, comment)
    {

      comment= is_void(comment) ? "" : strpart(comment, 1:70);
      _write, stream, address,

        (*pointer(swrite(format="%-8s %-70s\n", keyword, comment)))(1:80);
      address+= 80;
    }

    func _fitsPutEnd (stream, &address)
    {

      _fitsPutComment, stream, address, "END";
      empty= array(' ', 80);
      empty(0)= '\n';
      while ((address / 80) % 36) {
        _write, stream, address, empty;
        address+= 80;
      }
    }

    /*----------------------------------------------------------------------*/