/*****************************************************************************/
/*
                                HyperReader.c


A CGI-compliant script that reads (many) Bookreader-format documents and
presents them as HTML documents.

AND REMEMBER ... HyperReader can be used as a CGIplus script. This may require
the script to be purged from the server before new startup parameters come
into effect. Use a command like the following:

  HTTPD /DO=DCL=PURGE


SPECIFYING BOOKS
----------------
Books are specified in one of two ways, with the indicated precedence.

  1.  ?file= .......... request query string form field (VMS file spec)
  2.  /path/to/file ... path into VMS file spec via WWW_PATH_TRANSLATED

If a book has a known shelf extension (see storage 'BookshelfTypes') a 302
redirection is generated to the shelf processor (/hypershelf), likewise if a
book is not supplied.  The assumption being that the user knows no better than
to use HyperReader for everything!


BOOKS ACCESSED VIA DECNET
-------------------------

If any books are being accessed via DECnet, that is if a shelf contains
anything like

  book\NODE::DISK:[DECW$BOOK]BOOK.DECW$BOOK\A Book

the script may return the following error

  %RMS-E-NETBTS, network buffer too small for !UL byte record

Hyperreader attempts to cope with varying record sizes by always providing
enough space ;^) ... the "rab$w_usz = 32767;" in GetChunk(). Add the following
to the HyperReader support DCL procedure, WASD_ROOT:[SCRIPT]HYPERREADER.COM for
WASD, WWW_ROOT:[BIN]HYPERREADER.COM for OSU ...

  $ SET RMS /NETWORK_BLOCK_COUNT=127


OSU ENVIRONMENT
---------------
Script responses are returned in OSU "raw" mode; the script taking care of the
full response header and correctly carriage-controlled data stream, text or
binary!!  Uses the CGILIB.C to engage in the dialog phase generating, storing
and then making available the equivalent of CGI variables.


"VANILLA" CGI ENVIRONMENT
-------------------------
Primarily for the likes of Netscape FastTrack.  This environment can accomodate
CGI variables that are not prefixed with "WWW_" and do not supply "KEY_xxxxx"
or "FORM_xxxxx" (which must be derived from "QUERY_STRING").  Full HTTP stream
(non-parsed header) is assumed as not supported so all output occurs with a
CGI-compliant header line (e.g. "Status: 200 Success") and record-oriented
output.


ABOUT HYPERREADER
-----------------

Sorry about this utility.  When I (MGD) first began reverse-engineering the 
structure I didn't include copious notes (or even any ... well it was just
investigation), so now (late '94) I have only a vague idea of what some 
sections are all about.  Any new development has better commentary!  Also, no 
guarantees are given on any of this code, its all fairly tenuous.  The program 
has grown by accretion, more a vehicle of experimentation than a paradigm of 
software engineering.  This utility will probably always exist in a below-
version-1, as it will probably never be entirely satisfactory.  Well, enough 
of apologies! 

Code is included that should provide an informational message within the 
generated HTML whenever the utility encounters Bookreader formatting it cannot 
process.  During development/debugging this can be checked for clues. 

The code that generates the GIF output is, in part, derived from other works
from the PBM suite.  Some is copyright and used within the original licence
conditions:

 * GIF Image compression - LZW algorithm implemented with Trie type
 *                         structure.
 *                         Written by Bailey Brown, Jr.
 *                         last change May 24, 1990
 *                         file: compgif.c
 *
 *  You may use or modify this code as you wish, as long as you mention
 *  my name in your documentation.
 *
 *                  - Bailey Brown, Jr.


BOOKREADER DOCUMENTS ARE NOT PLAIN TEXT
---------------------------------------

They are not even marked-up text (in the same sense as HTML documents).  
Essentially they are a series of images pasted onto an X window.  Most of the 
images happen to be from fonts, but even then characters are not handled as 
text.  There is essentially no information within the file on the documentary 
function of any text in a book.  This is one reason for the poor performance 
of the internal Bookreader print functionality (it must adopt a similar 
approach, mapping into PostScript "pages"). 

Bookreader format is not public.

At least not that I could find.  Hence considerable effort at reverse-
engineering the structure (most originally during January 1992 fortunately, I 
wouldn't have the patience, time or interest now!)  In any case, the structure 
is complex and I don't pretend to understand it all ... so the server tends to 
break occasionally. 

The server attempts to map a bit-addressed format (Bookreader) onto a 
character-cell format (text-page).  The Bookreader format permits parts of a 
page to be generated non-sequentially, for example to place normal text, then 
place bolded text within it.  This can occur on a per-line, per-section or 
per-page basis.  So the entire "page" for a hypertext enviroment must be 
created by mapping into a "page" in memory.  Sometimes this mapping is not 
perfect and character strings get garbled or poorly proportioned. 

Text is presented in a fixed font.  This is due to two requirements: 

  1.  The mapping from bit-addressed to cell-addressed described
      above requires a fixed character-cell matrix. 

  2.  Maintaining correct layout for examples, tables, etc.  As the
      Bookreader format specifies the positioning of the document's
      components, rather than information on their function, relative
      location must be maintained, generally eliminating the use of the
      more aesthetically pleasing proportional fonts. 

Some massaging of the resulting page occurs.  This often involves educated 
guesswork.  For instance, single blank lines preceding a line beginning with a 
lower-case alphabetic are most often an artifact of the mapping process rather 
than intended line breaks, and can be eliminated to produce a better layout.  
Similarly, bulleted and numbered lists can be enhanced by ensuring a blank 
line exists between elements.  Unfortunately there are not many instances 
where the original intent can be clearly identified. 

As this is a stateless, client-server application any dynamic memory allocated 
will be released on image exit, and does not need to be explicitly released.


-POSSIBLE- BOOKREADER BOOK STRUCTURE (REVERSE-ENGINEERED SO FAR!):
------------------------------------------------------------------

First Record
------------
fixed size of 0x3fe
part type:                      short   0x00 (type=0x0001)
part length:                    long    0x02
number of parts in book         long    0x46
number of sections in book      long    0x4a
number of fonts in book         long    0x52
VBN (512) of last part          long    0x5a
position in VBN of last part    short   0x5e


Text Records
------------
variable size
part type:                      short   0x00 (type=0x0003)
part length:                    long    0x02
previous text part number       long    0x06
continued part                  long    0x0a (if > 1 then continues previous)
sections of text in this part   long    0x0e
previous chunk part number      long    0x12
chunk ends at part number       long    0x16
.
then from offset 0x1a
.
Sections within a text record
-----------------------------
section type                    short   0x00
.
if section type is 0x0012 (printable text)
section length                  long    0x02
section data 2                  long    0x06
this section number             long    0x0a
length of on-screen section     long    0x1e
section data 5                  long    0x22
section data 6                  long    0x26
section data 7                  long    0x2a
section data 8                  long    0x2e
section data 9                  long    0x32
section data 10                 long    0x36
.
if section type is 0x0013 (figure)
.
if section type is 0x0014 (graphic (hotspots, etc))
.
Printable text section
----------------------
type of text                    char    0x00
length of text section          char    0x01
horizonal position              short   0x02
vertical position               short   0x04
font number                     char    0x06
unknown                         short   0x07
.
text begins at offset 0x09, number of printable characters:
text length                     char    0x00
1..text length characters
then byte indicating decipoint(?) spacing between next printable characters


SectionXref Record
------------------
variable size
part type:                      short   0x00 (type=0x0006)
part length:                    long    0x02
then starting at offset 0x06:

Table of Contents Record
------------------------
variable size
part type:                      short   0x00 (type=0x0007)
part length:                    long    0x02
data 1                          long    0x06
data 2                          long    0x0a
data 3                          long    0x0e
data 4                          long    0x12
data 5                          long    0x16
data 6                          long    0x1a
data 7                          long    0x1e
then starting at offset 0x22:
a number of variable length units each comprising:
(the series of units is terminated by a byte of 0x00 at offset 0x06)
unknown                         short   0x00
unknown                         short   0x02
unknown                         short   0x04
null ('\0') terminted string at offset 0x06.


References to Parts (symbols) Record 
------------------------------------
variable size
part type:                      short   0x00 (type=0x000d)
part length:                    long    0x02
then starting at offset 0x06:
(1 .. number of sections in the book) series of 32 byte units, each:
null ('\0') terminted symbol string (can be just the number of the section)


Font Record
-----------
variable size
part type:                      short   0x00 (type=0x0009)
part length:                    long    0x02
then starting at offset 0x06:
(1 .. number of fonts in the book) series of variable byte units, each:
unknown                         short   0x00
unknown                         short   0x02
unknown                         short   0x04
font number (used in text)      short   0x06
null ('\0') terminted font name string


"Last" Record
-------------
variable size
part type:                      short   0x00 (type=0x0005)
part length:                    long    0x02
then starting at offset 0x06:
(1 .. number of parts in the book) series of 10 byte units, each:
VBN (512) of part               long    0x00
position in VBN of part         short   0x04
length of this part             long    0x06


CGI VARIABLES
-------------

Server ...

WWW_HTTP_IF_MODIFIED_SINCE      if "304 Not modified" are desired (optional)
WWW_HTTP_PRAGMA         "no-cache" (optional)
WWW_HTTP_REFERER        "close" button becomes active
WWW_PATH_INFO           URL path to book
WWW_PATH_TRANSLATED     VMS file specification for book
WWW_REQUEST_METHOD      "GET" only is supported
WWW_SCRIPT_NAME         path to script (e.g. "/HyperReader")
WWW_SERVER_CHARSET      default character set of server
WWW_SERVER_NAME         host on which the script is executing
WWW_SERVER_PORT         server port
WWW_SERVER_SOFTWARE     HTTPd identifying string

HyperReader ...

WWW_FORM_CHUNK          "chunk" number (section) of the book
WWW_FORM_FILE           VMS file name for book (when path cannot be supplied)
WWW_FORM_GRAPHIC        if not empty the get an image from the book
WWW_FORM_MASSAGE        if not empty then "massage" extra white space out
WWW_FORM_REFERER        overrides the HTTP_REFERER URL
WWW_FORM_TITLE          explicit title of book
WWW_FORM_VARREC         force record/block IO (debugging only)


QUALIFIERS
----------
/BUTTONS=       string containing button names
/CHARSET=       "Content-Type: text/html; charset=...", empty suppresses charset
/DBUG           turns on all "if (Debug)" statements (don't use with OSU)
/HYPERSHELF=    name of 'HyperShelf' script (defaults to "/hypershelf")
/[NO]ODS5       control extended file specification (basically for testing)


LOGICAL NAMES
-------------
HYPERREADER$DBUG       turns on all "if (Debug)" statements
HYPERREADER$PARAM      equivalent to (overrides) the command line
                       parameters/qualifiers (define as a system-wide logical)
HTTPD$GMT              timezone offset from Greenwich Mean Time
                       (e.g. "+09:30" and only needed if DTSS is not in use)


BUILD DETAILS
-------------
See BUILD_HYPERREADER.COM


COPYRIGHT
---------
Copyright (C) 1996-2020 Mark G.Daniel
This program comes with ABSOLUTELY NO WARRANTY.
This is free software, and you are welcome to redistribute it under the
conditions of the GNU GENERAL PUBLIC LICENSE, version 3, or any later version.
http://www.gnu.org/licenses/gpl.txt


HISTORY (also change SOFTWAREVN below!)
------- 
31-AUG-2020  MGD  v0.9.1, bugfix; eliminate &referer= XSS vulnerability
06-OCT-2014  MGD  v0.9.0, although now largely only of historical interest ...
                            ... a nod to the twenty-first century
                            (and happy 90th birthday Dad)
10-MAY-2005  MGD  v0.8.27, SWS 2.0 ignore query string components supplied as
                           command-line parameters differently to CSWS 1.2/3
23-DEC-2003  MGD  v0.8.26, minor conditional mods to support IA64
12-APR-2003  MGD  v0.8.25, link colour changed to 0000cc
20-AUG-2002  MGD  v0.8.24, allow for HTTP/1.1 'Cache-control:' field used
                           by Mozilla variants (instead of HTTP/1.0 'pragma:')
15-AUG-2002  MGD  v0.8.23, massage source code (can't resist a fiddle)
08-DEC-2001  MGD  v0.8.22, use RMS block IO for non-VAR record format books
01-JUL-2001  MGD  v0.8.21, add 'SkipParameters' for direct OSU support
28-OCT-2000  MGD  v0.8.20, use CGILIB object library
18-JAN-2000  MGD  v0.8.19, support extended file specifications (ODS-5),
                           improved GetChunk() record handling
07-AUG-1999  MGD  v0.8.18, use more of the CGILIB functionality
24-APR-1999  MGD  v0.8.17, use CGILIB.C,
                           standard CGI environment (Netscape FastTrack)
02-OCT-1998  MGD  v0.8.16, provide content-type "; charset=..."
08-AUG-1998  MGD  v0.8.15, redirection if looks like meant for shelf processor,
                           OSU output processing reworked,
                           bugfix; TimeSetTimezone() 'Seconds' unsigned->signed
06-AUG-1998  MGD  v0.8.14, accomodation for OSU ... reduce HTTP response
                           header carriage control from <CR><LF> to <LF> only
                           (OSU/IE4 combination problematic)
01-AUG-1998  MGD  v0.8.13, suppress table background colours if empty,
                           accomodations for OSU environment
28-APR-1998  MGD  v0.8.12, bit more playing around; book section descriptions
                           (table-of-contents, etc) now generated from book
                           content (non-English documents now work!!!!),
                           remove X-bitmap image generation,
                           some tidying up and cosmetic changes
13-MAR-1998  MGD  v0.8.11, TimeSetGmt() modified to be in line with HTTPd,
                           added check for too many chunks, sections and fonts
01-AUG-1997  MGD  v0.8.10, 'HttpHasBeenOutput' not initialized for CGIplus
20-JUL-1997  MGD  v0.8.9, added /BODY= qualifier (changed background colour)
07-JUL-1997  MGD  v0.8.8, CGIplus capable,
                          hopefully improved dynamic memory handling by setting
                          a minimum allocation reducing fragmentation
30-JUN-1997  MGD  v0.8.7, very minor changes to format,
                          "Pragma: no-cache" now overrides "If-Modified-Since:"
                          (as always, I look at this program and shudder :^)
16-AUG-1996  MGD  v0.8.6, presentation changes (XBMs to GIFs)
23-FEB-1996  MGD  v0.8.5, bugfix, after modifying the HTTPD$GMT format for the
                          HTTPd server I had forgotten about some scripts
12-OCT-1995  MGD  v0.8.4, 'path_translated' CGI variable now used to allow
                          books to be specified using HTTP URL syntax;
                          add 'Referer:', 'Last-Modified:'/'If-Modified-Since:'
02-AUG-1995  MGD  v0.8.3, changed detection of table of contents, figures, etc.,
                          to substring in function FirstChunk(); changes to
                          section processing, recognising figure types, etc.
24-MAY-1995  MGD  v0.8.2, minor changes for AXP compatibility
15-APR-1995  MGD  v0.8.1, made CGI-compliant, added "close" button functionality
12-NOV-1994  MGD  v0.8.0, breakthrough in understanding the layout of text,
                          I think!  All page layup functions revised.
01-NOV-1994  MGD  v0.7.0, support X-bitmap images, GIF images, X-bitmap ICON
                          buttons, direct RMS file access (much more efficient)
07-OCT-1994  MGD  v0.6.0, back from the dead ... for the hypertext environment
14-JAN-1992  MGD  v0.5.0, initial development as "BREAD v0.5.0" ...
                          because it was only half-baked  :^)
*/
/*****************************************************************************/

#define SOFTWAREVN "0.9.1"
#define SOFTWARENM "HYPERREADER"
#define SOFTWARECR "Copyright (C) 1996-2020 Mark G.Daniel"
#ifdef __ALPHA
#  define SOFTWAREID SOFTWARENM " AXP-" SOFTWAREVN
#endif
#ifdef __ia64
#  define SOFTWAREID SOFTWARENM " IA64-" SOFTWAREVN
#endif
#ifdef __VAX
#  define SOFTWAREID SOFTWARENM " VAX-" SOFTWAREVN
#endif
#ifdef __x86_64
   char SoftwareID [] = SOFTWARENM " X86-" SOFTWAREVN;
#endif

/* standard C header files */
#include <ctype.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>

/* VMS-related header files */
#include <descrip.h>
#include <file.h>
#include <iodef.h>
#include <libdef.h>
#include <lnmdef.h>
#include <rms.h>
#include <ssdef.h>
#include <stsdef.h>
#include <syidef.h>
#include <unixio.h>

/* application header file */
#include "enamel.h"
#include "hyperreader.h"
#include <cgilib.h>

#ifndef __VAX
#   pragma nomember_alignment
#endif

/***************/
/* definitions */
/***************/

#ifndef __VAX
#  ifndef NO_ODS_EXTENDED
#     define ODS_EXTENDED 1
      /* this is smaller than the technical maximum, but still quite large! */
#     define ODS_MAX_FILE_NAME_LENGTH 511
#     define ODS_MAX_FILESYS_NAME_LENGTH 264
#  endif
#endif
#define ODS2_MAX_FILE_NAME_LENGTH 255
#ifndef ODS_MAX_FILE_NAME_LENGTH
#  define ODS_MAX_FILE_NAME_LENGTH ODS2_MAX_FILE_NAME_LENGTH
#endif
#if ODS_MAX_FILE_NAME_LENGTH < ODS2_MAX_FILE_NAME_LENGTH
#  define ODS_MAX_FILE_NAME_LENGTH ODS2_MAX_FILE_NAME_LENGTH
#endif

#define BOOL int
#define true 1
#define false 0

#define VMSok(x) ((x) & STS$M_SUCCESS)
#define VMSnok(x) !(((x) & STS$M_SUCCESS))
#define VMSwarning(x) (((x) & 0x7) == STS$K_WARNING)
#define VMSerror(x) (((x) & 0x7) == STS$K_ERROR)
#define VMSinfo(x) (((x) & 0x7) == STS$K_INFO)
#define VMSfatal(x) (((x) & 0x7) == STS$K_SEVERR)
 
#define FI_LI __FILE__, __LINE__

#define MAX_OUTLINE_LENGTH 128
#define MAX_DIVISIONS 16

#define LINES_ON_PAGE_PREALLOCATE 10
/* maximum of 1 is hardly consecutive ... but experience shows it looks ok! */
#define MAX_CONSECUTIVE_BLANK_LINES 1

/*
   Whenever a non-printable charater is encountered the "interim" is
   substituted in the text.  Before the text is output these characters
   are used as markers for "lists" etc., as the text is "massaged".
   They are then replaced with the "final" character.  These appear most
   often to be "bullets" for lists.
*/
#define INTERIM_NON_ASCII_CHAR 0x7f
#define FINAL_NON_ASCII_CHAR '*'
/*
   For characters of any fonts not supported substitute this character.
   These are usually "graphic"/"symbol" characters/fonts.
*/
#define NON_SUPPORTED_CHAR '~'

/* seem to map bit-addressed to cell-addressed ok most of the time */
#define LINE_POSITIONING_UNITS 68
#define COLUMN_POSITIONING_UNITS 34

#define DEFAULT_BUTTONS \
"&larr;Previous$&#8630;Back=javascript:parent.history.go(-1)$\
Next&rarr;$Close$^Help=" DEFAULT_HYPERREADER_HELP

/**********/
/* macros */
/**********/

/*
   This macro generates an address (pointer) to an output buffer character.
   It allows a linear array of structures to be addressed with a line/column
   value as if it was a matrix.  A macro is used for speed (no function 
   overhead), and readability (easier to read than the computation each time).
*/
#define OUT_CHAR_AT(Line, Column) \
((struct OutCharStruct*)(OutPtr+(Line*MAX_OUTLINE_LENGTH)+Column))

/* try to avoid excessive fragmentation by specifying a minimum chunk */
#define MIN_HTML_STRING_ALLOC 128

/**************/
/* data types */
/**************/

struct ChunkDataStruct
{
   unsigned long  VBN;
   unsigned short  VBNbyte;
   unsigned long  Length;
};

struct OutCharStruct
{
   char  c;
   char  *htptr;
};

struct FontStruct
{
   int  FontSize;
   BOOL  FontBold;
   BOOL  FontItalic;
   BOOL  FontUnsupported;
};

/* my divisions are the "Contents", "Index", etc. */
struct BookDivisionStruct
{
   char  *DescriptionPtr;
   int  ChunkNumber;
};

/******************/
/* global storage */
/******************/

char ErrorBookType [] = "Doesn't look like a book type!",
     ErrorCalloc [] = "allocating memory.",
     ErrorCallocBytes [] = "memory allocation of %d bytes failed",
     ErrorChunkRead [] = "chunk read problem.",
     ErrorImageSpecification [] = "Image specification incorrect.",
     ErrorRequestedChunk [] = "Could not find requested part of book!",
     ErrorStringOverflow [] = "String overflow!",
     ErrorTOC [] = "Could not locate table-of-contents!",
     ErrorTooManyDivisions [] =
"Too many &quot;divisions&quot; in book! \
Recompile with increased MAX_DIVISIONS.";

char  *BookshelfTypes [] = { ".ODL", ".BKS", ".DECW$BOOKSHELF", "" };
char  *BookTypes [] = { ".BKB", ".DECW$BOOK", "" };

char  Utility [] = "HYPERREADER";

BOOL  BookFileVarRec,
      Debug,
      DebugBytes,
      DebugFigures,
      DebugFonts,
      DebugGraphics,
      DebugHotspots,
      DebugChunks,
      DebugSections,
      DebugSymbols,
      DebugText,
      DoMassage,
      DoMassageOutput,
      ErrorReported,
      EnhanceText,
      IsCgiPlus,
      OsuEnvironment,
      StdCgiEnvironment,
      TimeAheadOfGmt;

int  BookFileLongestRecordLength,
     ChunkNumber,
     DynamicMemoryAllocated,
     EndGlobalVertical,
     GetChunkNumber,
     GlobalVertical,
     GlobalHorizontal,
     LastLineOnPage,
     LinesOnPage,
     LinesOutputToClient,
     NextChunkNumber,
     OdsExtended,
     PreviousChunkNumber,
     TablesChunkNumber,
     TotalBytesRead,
     TotalBytesWritten;

#define TBW TotalBytesWritten

long  NumberOfChunks,
      NumberOfSections,
      NumberOfFonts,
      CurrentVBN,
      LastVBN,
      LastVBNlen;

unsigned long  IfModifiedSinceBinaryTime [2],
               TimeGmtDeltaBinary [2];

short  LastVBNbyte;

char  BookSpecifiedByFile [ODS_MAX_FILE_NAME_LENGTH+1],
      BookTitle [256],
      ContentTypeCharset [64],
      DefaultButtons [] = DEFAULT_BUTTONS,
      DefaultStyle [] = DEFAULT_HYPERREADER_STYLE,
      ExpBookFileName [ODS_MAX_FILE_NAME_LENGTH+1],
      LastModifiedGmDateTime [32],
      SoftwareCopy [] = SOFTWARECR,
      SoftwareID [48],
      TimeGmtString [32],
      TimeGmtVmsString [32],
      UrlEncReferer [1024],
      UrlEncTitle [512];

char  *BookFileNamePtr,
      *ButtonsPtr = DefaultButtons,
      *CgiEnvironmentPtr,
      *CgiFormChunkPtr,
      *CgiFormFilePtr,
      *CgiFormGraphicPtr,
      *CgiFormMassagePtr,
      *CgiFormRefererPtr,
      *CgiFormTitlePtr,
      *CgiFormVarRecPtr,
      *CgiHttpCacheControlPtr,
      *CgiHttpHostPtr,
      *CgiHttpPragmaPtr,
      *CgiHttpIfModifiedSincePtr,
      *CgiPathInfoPtr,
      *CgiPathTranslatedPtr,
      *CgiQueryStringPtr,
      *CgiRequestMethodPtr,
      *CgiRequestSchemePtr,
      *CgiScriptNamePtr,
      *CgiServerNamePtr,
      *CgiServerSoftwarePtr,
      *CgiServerPortPtr,
      *CharsetPtr,
      *CliCharsetPtr,
      *HyperShelfScriptNamePtr,
      *StyleSheetPtr = "",
      *UrlDoMassage,
      *UrlEncodedCgiPathInfoPtr;

long  *SectionChunkNumbersArrayPtr;

struct FontStruct  *FontArrayPtr;

struct ChunkDataStruct  *ChunkArrayPtr;

/* HyperReader divisions are the "Contents", "Index", etc. */
struct BookDivisionStruct  BookDivision [MAX_DIVISIONS+1];
int  BookDivisionCount;

struct OutCharStruct  *OutPtr;

unsigned char  *ChunkBufferPtr;
int  PreviousChunkLength;
unsigned char  *ChunkPtr;
unsigned char  *EndChunkPtr;

/* the five long words in the part header */
long  PreviousChunk;
long  SectionsInChunk;
long  PreviousTopicChunk;
long  NextTopicChunk;

long  FinalChunkNumber;
long  CurrentSeek;
long  CurrentChunkLength;
int  CurrentChunk;
short  CurrentChunkType;

struct ChunkDataStruct  FirstChunk,
                        FontChunk,
                        ImageChunk,
                        LastChunk,
                        SectionXrefChunk,
                        SectionHeadingsChunk,
                        SymbolsChunk;

struct FAB  BookFileFab;
struct RAB  BookFileRab;
#ifdef ODS_EXTENDED
   struct NAML  BookFileNaml;
#endif /* ODS_EXTENDED */
struct NAM  BookFileNam;
struct XABDAT  BookFileXabDat;
struct XABFHC  BookFileXabFhc;

/***********************/
/* function prototypes */
/***********************/

char* HtmlString (char*, int);
char* ButtonBarButton (char*, char*, int, BOOL);
char* XSSuspect (char*);

/*****************************************************************************/
/*
'argc' and 'argv' are only required to support CgiLibEnvironmentInit(), in
particular the OSU environment.
*/

main
(
int argc,
char *argv[]
)
{
   int  status;

   /*********/
   /* begin */
   /*********/

   sprintf (SoftwareID, "%s (%s)", SOFTWAREID, CgiLibEnvironmentVersion());

   if (getenv ("HYPERREADER$DBUG")) Debug = true;
   if (Debug) CgiLibResponseHeader (200, "text/plain");
   CgiLibEnvironmentSetDebug (Debug);

   CgiLibEnvironmentInit (argc, argv, false);

   GetParameters ();

   CgiLibResponseSetCharset (CliCharsetPtr);
   CgiLibResponseSetSoftwareID (SoftwareID);
   CgiLibResponseSetErrorMessage ("Reported by HyperReader");

   if (StyleSheetPtr[0])
   {
      char *cptr = calloc (1, 64+strlen(StyleSheetPtr));
      sprintf (cptr, "<link rel=\"stylesheet\" \
type=\"text/css\" href=\"%s\">\n",
               StyleSheetPtr);
      StyleSheetPtr = cptr;
   }

   IsCgiPlus = CgiLibEnvironmentIsCgiPlus ();
   CgiEnvironmentPtr = CgiLibEnvironmentName ();

#ifdef ODS_EXTENDED
   OdsExtended = (GetVmsVersion() >= 72);
   ENAMEL_NAML_SANITY_CHECK
#endif /* ODS_EXTENDED */

   if (IsCgiPlus)
   {
      for (;;)
      {
         /* block waiting for the next request */
         CgiLibVar ("");
         if (Debug) fputs ("Content-Type: text/plain\n", stdout);
         ProcessRequest ();
         CgiLibCgiPlusEOF ();
      }
   }
   else
      ProcessRequest ();

   exit (SS$_NORMAL);
}

/*****************************************************************************/
/*
Get "command-line" parameters, whether from the command-line or from a
configuration symbol or logical containing the equivalent.  OSU scripts have
the 'method', 'url' and 'protocol' supplied as P1, P2, P3 (these being detected
and used by CGILIB), and are of no interest to this function.
*/

GetParameters ()

{
   static unsigned long  Flags = 0;
   static char  CommandLine [256];

   int  status,
        SkipParameters;
   unsigned short  Length;
   char  ch;
   char  *aptr, *cptr, *clptr, *sptr;
   $DESCRIPTOR (CommandLineDsc, CommandLine);

   /*********/
   /* begin */
   /*********/

   if (!(clptr = getenv ("HYPERREADER$PARAM")))
   {
      /* get the entire command line following the verb */
      if (VMSnok (status =
          lib$get_foreign (&CommandLineDsc, 0, &Length, &Flags)))
         exit (status);
      (clptr = CommandLine)[Length] = '\0';
   }

   /* if [C]SWS (VMS Apache) */
   if (CgiLibEnvironmentIsApache())
   {
      /* CSWS 1.2/3 look for something non-space outside of quotes */
      for (cptr = clptr; *cptr; cptr++)
      {
         if (isspace(*cptr)) continue;
         if (*cptr != '\"') break;
         cptr++;
         while (*cptr && *cptr != '\"') cptr++;
         if (*cptr) cptr++;
      }
      /* CSWS 1.2/3 if nothing outside of quotes then ignore command line */
      if (!*cptr) return;
      /* SWS 2.0 doesn't begin with /APACHE from DCL procedure wrapper */
      if (!strsame (cptr, "/APACHE", 7)) return;
   }

   /* if OSU environment then skip P1, P2, P3 */
   if (CgiLibEnvironmentIsOsu())
      SkipParameters = 3;
   else
      SkipParameters = 0;

   aptr = NULL;
   ch = *clptr;
   for (;;)
   {
      if (aptr) *aptr = '\0';
      if (!ch) break;

      *clptr = ch;
      if (Debug) fprintf (stdout, "clptr |%s|\n", clptr);
      while (*clptr && isspace(*clptr)) *clptr++ = '\0';
      aptr = clptr;
      if (*clptr == '/') clptr++;
      while (*clptr && !isspace (*clptr) && *clptr != '/')
      {
         if (*clptr != '\"')
         {
            clptr++;
            continue;
         }
         cptr = clptr;
         clptr++;
         while (*clptr)
         {
            if (*clptr == '\"')
               if (*(clptr+1) == '\"')
                  clptr++;
               else
                  break;
            *cptr++ = *clptr++;
         }
         *cptr = '\0';
         if (*clptr) clptr++;
      }
      ch = *clptr;
      if (*clptr) *clptr = '\0';
      if (Debug) fprintf (stdout, "aptr |%s|\n", aptr);
      if (!*aptr) continue;

      if (SkipParameters)
      {
         SkipParameters--;
         continue;
      }

      if (strsame (aptr, "/APACHE", 4))
      {
         /* just skip this marker for command-line parameters under SWS 2.0 */
         continue;
      }
      if (strsame (aptr, "/BUTTONS=", 4))
      {
         for (cptr = aptr; *cptr && *cptr != '='; cptr++);
         if (*cptr) cptr++;
         if (*cptr == '+')
         {
            /* append buttons to defaults */
            aptr = calloc (1, strlen(ButtonsPtr)+strlen(cptr)+2);
            strcpy (aptr, ButtonsPtr);
            strcat (aptr, "$");
            strcat (aptr, cptr+1);
            ButtonsPtr = aptr;
         }
         else
            ButtonsPtr = cptr;
         continue;
      }
      if (strsame (aptr, "/CHARSET=", 4))
      {
         for (cptr = aptr; *cptr && *cptr != '='; cptr++);
         if (*cptr) cptr++;
         CliCharsetPtr = cptr;
         continue;
      }
      if (strsame (aptr, "/DBUG", -1))
      {
         Debug = true;
         continue;
      }
      if (strsame (aptr, "/HYPERSHELF=", 4))
      {
         for (cptr = aptr; *cptr && *cptr != '='; cptr++);
         if (*cptr) cptr++;
         HyperShelfScriptNamePtr = cptr;
         continue;
      }
      if (strsame (aptr, "/ODS5", 5))
      {
         OdsExtended = true;
         continue;
      }
      if (strsame (aptr, "/NOODS5", 7))
      {
         OdsExtended = false;
         continue;
      }
      if (strsame (aptr, "/STYLE=", 4))
      {
         for (cptr = aptr; *cptr && *cptr != '='; cptr++);
         if (*cptr) cptr++;
         StyleSheetPtr = cptr;
         continue;
      }

      /* various levels of debugging (sigh!) */
/*
      if (strsame (aptr, "/BYT", 4)) { DebugBytes = true; continue; }
      if (strsame (aptr, "/CHU", 4)) { DebugChunks = true; continue; }
      if (strsame (aptr, "/DBU", 4)) { Debug = true; continue; }
      if (strsame (aptr, "/DEB", 4)) { Debug = true; continue; }
      if (strsame (aptr, "/FIG", 4)) { DebugFigures = true; continue; }
      if (strsame (aptr, "/FON", 4)) { DebugFonts = true; continue; }
      if (strsame (aptr, "/GRA", 4)) { DebugGraphics = true; continue; }
      if (strsame (aptr, "/HOT", 4)) { DebugHotspots = true; continue; }
      if (strsame (aptr, "/SEC", 4)) { DebugSections = true; continue; }
      if (strsame (aptr, "/SYM", 4)) { DebugSymbols = true; continue; }
      if (strsame (aptr, "/TEX", 4)) { DebugText = true; continue; }
*/

      if (*aptr != '/')
      {
         fprintf (stdout, "%%%s-E-MAXPARM, too many parameters\n \\%s\\\n",
                  Utility, aptr);
         exit (STS$K_ERROR | STS$M_INHIB_MSG);
      }
      else
      {
         fprintf (stdout, "%%%s-E-IVQUAL, unrecognized qualifier\n \\%s\\\n",
                  Utility, aptr+1);
         exit (STS$K_ERROR | STS$M_INHIB_MSG);
      }
   }
}

/*****************************************************************************/
/*
*/ 

ProcessRequest ()

{
   int  idx, status;
   char  *cptr;

   /*********/
   /* begin */
   /*********/

   if (Debug) fprintf (stdout, "ProcessRequest()\n");

   CgiEnvironmentPtr = CgiLibEnvironmentName ();

   if (!getenv ("HYPERREADER$DBUG"))
      Debug = false;
   else
      Debug = true;

   ErrorReported = false;

   BookDivisionCount = DynamicMemoryAllocated =
      EndGlobalVertical = GlobalVertical = GlobalHorizontal =
      LastLineOnPage = LinesOnPage = LinesOutputToClient =
      NextChunkNumber = NextTopicChunk = PreviousChunkNumber =
      PreviousTopicChunk = TablesChunkNumber = 0;

   /*************************/
   /* get the CGI variables */
   /*************************/

   /* initialize a timer for establishing page composition statistics */
   lib$init_timer (0);

   CgiServerSoftwarePtr = CgiLibVar ("WWW_SERVER_SOFTWARE");
   CgiRequestMethodPtr = CgiLibVar ("WWW_REQUEST_METHOD");
   if (strcmp (CgiRequestMethodPtr, "GET"))
   {
      CgiLibResponseHeader (501, "text/html");
      fprintf (stdout, "Not implemented!\n");
      return;
   }

   CgiPathInfoPtr = CgiLibVar ("WWW_PATH_INFO");
   CgiPathTranslatedPtr = CgiLibVar ("WWW_PATH_TRANSLATED");
   CgiScriptNamePtr = CgiLibVar ("WWW_SCRIPT_NAME");
   CgiQueryStringPtr = CgiLibVar ("WWW_QUERY_STRING");
   CgiHttpCacheControlPtr = CgiLibVar ("WWW_HTTP_CACHE_CONTROL");
   CgiHttpIfModifiedSincePtr = CgiLibVar ("WWW_HTTP_IF_MODIFIED_SINCE");
   CgiHttpPragmaPtr = CgiLibVar ("WWW_HTTP_PRAGMA");

   CgiFormFilePtr = CgiLibVar ("WWW_FORM_FILE");
   if (!CgiFormFilePtr[0]) CgiFormFilePtr = CgiLibVar ("WWW_FORM_BOOK");
   CgiFormChunkPtr = CgiLibVar ("WWW_FORM_CHUNK");
   CgiFormGraphicPtr = CgiLibVar ("WWW_FORM_GRAPHIC");
   CgiFormMassagePtr = CgiLibVar ("WWW_FORM_MASSAGE");
   CgiFormTitlePtr = CgiLibVar ("WWW_FORM_TITLE");

   /* this is really only for experimentation/debugging */
   CgiFormVarRecPtr = CgiLibVar ("WWW_FORM_VARREC");

   if (VMSnok (status = TimeSetGmt ()))
   {
      if (status != SS$_NOLOGNAM)
      {
         CgiLibResponseError (FI_LI, status, "GMT offset");
         exit (SS$_NORMAL);
      }
   }

   if (CgiHttpIfModifiedSincePtr[0])
   {
      if (VMSnok (HttpGmTime (CgiHttpIfModifiedSincePtr, 
                              &IfModifiedSinceBinaryTime)))
      {
         if (Debug) fprintf (stdout, "If-Modified-Since: NBG!\n");
         IfModifiedSinceBinaryTime[0] = IfModifiedSinceBinaryTime[0] = 0;
         CgiHttpIfModifiedSincePtr = "";
      }
   }

   /***********/
   /* process */
   /***********/

   if (!HyperShelfScriptNamePtr)
   {
      if (CgiLibEnvironmentIsWasd() ||
          CgiLibEnvironmentIsCgiPlus())
         HyperShelfScriptNamePtr = DEFAULT_WASD_HYPERSHELF;
      else
      if (CgiLibEnvironmentIsOsu())
         HyperShelfScriptNamePtr = DEFAULT_OSU_HYPERSHELF;
      else
         HyperShelfScriptNamePtr = DEFAULT_CGI_HYPERSHELF;
   }

   UrlEncodedCgiPathInfoPtr =
      (char*)CgiLibUrlEncode (CgiPathInfoPtr, -1, NULL, 0);

   if (CgiFormFilePtr[0])
   {
      BookFileNamePtr = CgiFormFilePtr;
      sprintf (BookSpecifiedByFile, "file=%s&", CgiFormFilePtr);
   }
   else
   {
      BookFileNamePtr = CgiPathTranslatedPtr;
      BookSpecifiedByFile[0] = '\0';
   }
   if (Debug) fprintf (stdout, "BookFileNamePtr |%s|\n", BookFileNamePtr);

   if (BookFileNamePtr[0])
   {
      /* find file type (extension) */
      for (cptr = BookFileNamePtr; *cptr; cptr++);
      if (*(cptr-1) == ';') *(cptr-1) = '\0';
      while (cptr > BookFileNamePtr && *cptr != '.' &&
             *cptr != ']' && *cptr != ':') cptr--;
      if (*cptr == '.')
      {
         /* check if looks like a bookshelf type */
         for (idx = 0; BookshelfTypes[idx][0]; idx++)
         {
            if (Debug) fprintf (stdout, "|%s|%s|\n", BookshelfTypes[idx], cptr);
            if (!strsame (BookshelfTypes[idx], cptr, -1)) continue;
            /* looks like a shelf type! */
            BookFileNamePtr = "";
            break;
         }
      }
   }
   if (Debug) fprintf (stdout, "BookFileNamePtr |%s|\n", BookFileNamePtr);

   if (!BookFileNamePtr[0])
   {
      /*******************************/
      /* redirect to shelf processor */
      /*******************************/

      CgiLibResponseRedirect ("%!%s%s%s%s",
         HyperShelfScriptNamePtr, CgiPathInfoPtr,
         CgiQueryStringPtr[0] ? "?" : "", CgiQueryStringPtr);

      return;
   }
   /* check if looks like book type */
   for (idx = 0; BookTypes[idx][0]; idx++)
   {
      if (Debug) fprintf (stdout, "|%s|%s|\n", BookTypes[idx], cptr);
      if (!strsame (BookTypes[idx], cptr, -1)) continue;
      /* looks like a book type! */
      break;
   }
   if (!BookTypes[idx][0])
   {
      CgiLibResponseError (FI_LI, 0, ErrorBookType);
      return;
   }

   CgiFormRefererPtr = CgiLibVar ("WWW_FORM_REFERER");
   if (!CgiFormRefererPtr[0])
      CgiFormRefererPtr = CgiLibVar ("WWW_HTTP_REFERER");
   if (cptr = XSSuspect (CgiFormRefererPtr))
   {
      CgiLibResponseError (FI_LI, 0, cptr);
      return;
   }
   if (CgiFormRefererPtr[0])
      CgiLibUrlEncode (CgiFormRefererPtr, -1,
                       UrlEncReferer, sizeof(UrlEncReferer));
   else
      UrlEncReferer[0] = '\0';

   EnhanceText = true;
   TotalBytesRead = TBW = 0;

   if (CgiFormTitlePtr[0])
      CgiLibUrlEncode (CgiFormTitlePtr, -1, UrlEncTitle, -1);

   if (toupper(CgiFormMassagePtr[0]) == 'N')
      DoMassage = false;
   else
      DoMassage = true;

   if (CgiFormGraphicPtr[0])
      ImageReader();
   else
      BookReader();

   /************/
   /* clean up */
   /************/

   if (BookFileFab.fab$w_ifi) sys$close (&BookFileFab, 0, 0);

   if (UrlEncodedCgiPathInfoPtr) free (UrlEncodedCgiPathInfoPtr);
   UrlEncodedCgiPathInfoPtr = NULL;

   if (ChunkBufferPtr) free (ChunkBufferPtr);
   ChunkBufferPtr = NULL;
   PreviousChunkLength = 0;

   if (ChunkArrayPtr) free (ChunkArrayPtr);
   ChunkArrayPtr = NULL;

   if (FontArrayPtr) free (FontArrayPtr);
   FontArrayPtr = NULL;

   if (SectionChunkNumbersArrayPtr) free (SectionChunkNumbersArrayPtr);
   SectionChunkNumbersArrayPtr = NULL;

   for (idx = 1; idx <= BookDivisionCount; idx++)
   {
      if (BookDivision[idx].DescriptionPtr)
         free (BookDivision[idx].DescriptionPtr);
      BookDivision[idx].DescriptionPtr = NULL;
   }

   PageSize (-1);

   MassageOutputLine (NULL, NULL, NULL);

   OutputPage (true);
}

/*****************************************************************************/
/*
Retrieve a BRF (BookReader Format) image from within a document and send it to 
the client.  Bookreader images are essentially uncompressed bitmaps.  
Horizontal and vertical coordinates position on the page, and height and width 
information allows the image to be reconstructed.
*/ 

ImageReader ()

{
   unsigned long  ChunkLength = 0,
                  DataLength,
                  Horiz,
                  ImageChunkOffset = 0,
                  StartOfImageOffset = 0,
                  VBN = 0,
                  VBNbyte = 0;
   unsigned short  HeightPixels,
                   WidthPixels,
                   SectionType;
   float  FloatWidth;

   /*********/
   /* begin */
   /*********/

   if (Debug) fprintf (stdout, "ImageReader() |%s|\n", CgiFormGraphicPtr);

   /*
      The four numbers in the 'CgiFormGraphicPtr' are:
      (note that parts one and two constitute the RMS RFA of the record)

      1.  Virtual Block Number of the start of the part
      2.  starting byte position in the VBN
      3.  total length of the chunk
      4.  offset from start of chunk the image data begins

      This will allow the image to be retrieved very quickly by merely
      opening the file, reading the part (one or more records) via the RFA
      and jumping in to process the image data from the offset.
   */

   sscanf (CgiFormGraphicPtr, "%d,%d,%d,%d",
           &VBN, &VBNbyte, &ChunkLength, &ImageChunkOffset);

   /* bit of a sanity check, VBNbyte can be zero of course, others cannot */
   if (!VBN || VBNbyte > 511 || !ChunkLength || !ImageChunkOffset)
   {
      CgiLibResponseError (FI_LI, 0, ErrorImageSpecification);
      return;
   }

   if (VMSnok (OpenBookFile ())) return;

   ImageChunk.VBN = VBN;
   ImageChunk.VBNbyte = (unsigned short)VBNbyte;
   ImageChunk.Length = ChunkLength;
   if (VMSnok (GetChunk (&ImageChunk)))
      return;

   /* this is a good sanity check on the image specification */
   if (ChunkLength != CurrentChunkLength)
   {
      CgiLibResponseError (FI_LI, 0, ErrorImageSpecification);
      return;
   }

   ChunkPtr = ChunkBufferPtr + ImageChunkOffset;

   /*********************/
   /* process the image */
   /*********************/

   SectionType = *(unsigned short*)ChunkPtr;

   if (SectionType == 18)
   {
      /*******************************************************************/
      /* the section 18 processing is ALL CONJECTURE ... 03-AUG-1995 MGD */
      /*******************************************************************/

      StartOfImageOffset = 42;
      DataLength = *(unsigned long*)(ChunkPtr+2) - StartOfImageOffset;
      /* fudge factor of 5.33 seems to work!? (must be another way!) */
      WidthPixels =
      (unsigned short)((float)*(unsigned long*)(ChunkPtr+26) / 5.33);
      HeightPixels = DataLength / (WidthPixels / 8);
   }
   else
   if (SectionType == 19)
   {
      StartOfImageOffset = 50;
      DataLength = *(unsigned long*)(ChunkPtr+2);
      WidthPixels = *(unsigned short*)(ChunkPtr+46);
      HeightPixels = *(unsigned short*)(ChunkPtr+48);
   }

   if (DebugFigures)
   {
      DumpLongWords ("IMAGE: ", ChunkPtr+2, 12);
      fprintf (stdout, "DataLength : 0x%08.08lx (%d)\n",
               DataLength, DataLength);
      fprintf (stdout, "WidthPixels : %d\n", WidthPixels);
      fprintf (stdout, "HeightPixels : %d\n", HeightPixels);
   }

   GifImage (WidthPixels, HeightPixels,
             ChunkPtr+StartOfImageOffset, DataLength-StartOfImageOffset);
}

/*****************************************************************************/
/*
Construct the page (requested portion of the book), output HTML to provide a 
page (book title) heading, etc., output the page constructed, then some more 
HTML to finish off the page.
*/ 

BookReader ()

{
   int  idx;
   char  *sptr;
   char  EscapedBookFileName [ODS_MAX_FILE_NAME_LENGTH+1],
         UnixDateTime [32];
   unsigned long  UnixTime;
   struct tm  *UnixTmPtr;

   /*********/
   /* begin */
   /*********/

   if (Debug) fprintf (stdout, "BookReader()\n");

   if (VMSnok (OpenBookFile ())) return;

   /* 
      This string is added to an HREF="..." link after the part number.
      It propagates whether the book contents are being "massaged" or not.
   */
   if (DoMassageOutput = DoMassage)
      UrlDoMassage = "";
   else
      UrlDoMassage = "&massage=no";

   /************************/
   /* process book details */
   /************************/

   /* get information of the physical "structure" of the book */
   GetFirstChunk ();
   GetLastChunk ();
   GetFonts ();
   GetSectionXref ();

   GetChunkNumber = atol (CgiFormChunkPtr);
   if (Debug)
      fprintf (stdout,
         "CgiFormChunkPtr |%s| GetChunkNumber: %d\n",
          CgiFormChunkPtr, GetChunkNumber);
   if (!GetChunkNumber)
   {
      if (!BookDivisionCount)
      {
         CgiLibResponseError (FI_LI, 0, ErrorTOC);
         return;
      }
      GetChunkNumber = BookDivision[1].ChunkNumber;
   }

   if (GetChunkNumber < 1 || GetChunkNumber > NumberOfChunks)
   {
      CgiLibResponseError (FI_LI, 0, ErrorRequestedChunk);
      return;
   }

   for (idx = 1; idx <= BookDivisionCount; idx++)
   {
      if (BookDivision[idx].ChunkNumber == GetChunkNumber)
      {
         /* 
             These pages do not have bold/italic-text tags and are not
             output-massaged at all.  They look and "perform" better that way!
         */
         DoMassageOutput = false;
         break;
      }
   }

   /*********************/
   /* HTTP header, etc. */
   /*********************/

   time (&UnixTime);
   UnixTmPtr = localtime (&UnixTime);
   if (!strftime (UnixDateTime, sizeof(UnixDateTime),
                  "%a, %d %b %Y %T", UnixTmPtr))
      strcpy (UnixDateTime, "[error]");
   if (Debug) fprintf (stdout, "UnixDateTime |%s|\n", UnixDateTime);

   TBW += CgiLibResponseHeader (200, "text/html",
                               "Last-Modified: %s\n", LastModifiedGmDateTime);

   CgiLibHtmlEscape (ExpBookFileName, -1,
                     EscapedBookFileName, sizeof(ExpBookFileName));

   TBW += fprintf (stdout,
"<!DOCTYPE html>\n\
<html>\n\
<head>\n\
<meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n\
<meta name=\"generator\" content=\"%s\">\n\
<meta name=\"copyright\" content=\"%s - GPL licensed\">\n\
<meta name=\"environment\" content=\"%s\">\n\
<meta name=\"date\" content=\"%s\">\n\
<meta name=\"file\" content=\"%s (%s)\">\n",
      SoftwareID, SoftwareCopy,
      CgiEnvironmentPtr, UnixDateTime,
      EscapedBookFileName, BookFileVarRec ? "record" : "block");

   /******************/
   /* construct page */
   /******************/

   if (VMSnok (GetChunk (&(ChunkArrayPtr[GetChunkNumber]))))
      return;

   ProcessChunk ();

   PreviousChunkNumber = PreviousTopicChunk;
   if (NextTopicChunk && NextTopicChunk < NumberOfChunks)
       NextChunkNumber = NextTopicChunk;

   /***************/
   /* output page */
   /***************/

   TBW += fprintf (stdout,
"<title>HyperReader ... %s</title>\n\
<style type=\"text/css\">\n\
%s\
</style>\n\
%s\
</head>\n\
<body>\n\
<div class=\"header\">%s</div>\n",
      BookTitle,
      DefaultStyle,
      StyleSheetPtr,
      BookTitle);

   ButtonBar (1);

   TBW += fprintf (stdout, "<pre>");

   OutputPage (false);

   TBW += fprintf (stdout, "</pre>\n");

   ButtonBar (2);

   Statistics ();
   TBW += fprintf (stdout, "</body>\n</html>\n");
}

/*****************************************************************************/
/*
*/

Statistics ()

{
   static char  StatisticsFao [] =
"<!!--\n\
TIMES  layup: !%T\n\
       cpu:   00:!2ZL:!2ZL.!2ZL\n\
BYTES  read from book: !UL\n\
       written to client: !UL (approx.)\n\
       memory allocated: !UL\n\
LINES  allocated for page: !UL\n\
       last on page: !UL\n\
       output to client: !UL\n\
-->\n";

   static long  LibElapsedTime = 1,
                LibCpuTime = 2;

   unsigned long  ElapsedTime [2];
   unsigned long  CpuTime;
   unsigned short  Length;
   char  String [512];
   $DESCRIPTOR (StatisticsFaoDsc, StatisticsFao);
   $DESCRIPTOR (StringDsc, String);

   /*********/
   /* begin */
   /*********/

   if (Debug) fprintf (stdout, "Statistics()\n");

   lib$stat_timer (&LibElapsedTime, &ElapsedTime, 0);
   lib$stat_timer (&LibCpuTime, &CpuTime, 0);

   sys$fao (&StatisticsFaoDsc, &Length, &StringDsc,
            &ElapsedTime, CpuTime/6000, CpuTime/100, CpuTime%100,
            TotalBytesRead,
            /* this is a close approximation, total so far plus guess-timate */
            TBW + sizeof(StatisticsFao),
            DynamicMemoryAllocated,
            LinesOnPage, LastLineOnPage, LinesOutputToClient);
   String[Length] = '\0';

   fputs (String, stdout);
}

/*****************************************************************************/
/*
The "page" of output has been constructed in the matrix of 'OutCharStruct'
elements.

Scan through this as if it was a page of text, starting at line one and 
character one, moving across the columns to the last character, then moving to 
the next line, etc. 

The page 'OutCharStruct' elements consist of a single character (any character 
mapped from the Bookreader source) and a pointer to 'char'.  This pointer 
will, most often, be NULL.  However, when HTML formatting (e.g. bolding) or 
linkage (e.g. a Bookreader "hotspot") is required a string is dynamically 
allocated to contain the required HTML and pointed to by this pointer in the 
appropriate character location.

If the HTML pointer exists copy the string to the output buffer first then any 
Bookreader character in the character variable.
*/ 

OutputPage (BOOL Reset)

{
   static int  PreBlankLineCount = 0;

   BOOL  TerminateAndOutputLine;
   int  lcnt, ccnt,
        PostBlankLineCount = 0,
        LineLength;
   char  *cptr, *eptr, *lptr, *zptr;
   char  Line [8192];
   struct OutCharStruct  *ocptr;

   /*********/
   /* begin */
   /*********/

   if (Debug)
      fprintf (stdout, "OutputPage() %d/%d\n", LastLineOnPage, LinesOnPage);

   if (Reset)
   {
      /* reset after use */
      PreBlankLineCount = 0;
      return;
   }

   /*******************/
   /* loop thru lines */
   /*******************/

   for (lcnt = 1; lcnt <= LastLineOnPage; lcnt++)
   {
      if (Debug) fprintf (stdout, "%d\n", lcnt);

      /* 'eptr' points at last space character copied into output 'Line' */
      zptr = (eptr = lptr = Line) + sizeof(Line)-8;
      /* point at the first character structure in the output buffer */
      ocptr = OUT_CHAR_AT(lcnt,0);

      /************************/
      /* loop thru characters */
      /************************/

      for (ccnt = 0; ccnt < MAX_OUTLINE_LENGTH; ccnt++)
      {
         if (lptr >= zptr)
         {
            CgiLibResponseError (FI_LI, 0, ErrorStringOverflow);
            return;
         }
         /* insert any HTML associated with the character position */
         cptr = ocptr->htptr;
         if (cptr)
         {
            while (*cptr && lptr < zptr) *lptr++ = *cptr++;
            /* be sure to terminate after any included HTML */
            eptr = lptr;
         }
         /* add the character from buffer, filter HTML-forbidden characters */
         switch (ocptr->c)
         {
            case '&' : strcpy (lptr, "&amp;"); lptr += 4; break;
            case '<' : strcpy (lptr, "&lt;"); lptr += 3; break;
            case '>' : strcpy (lptr, "&gt;"); lptr += 3; break;
            default : *lptr = ocptr->c;
         }
         /* if the character was not a space then note the position */
         if (*lptr != ' ') eptr = lptr+1;
         lptr++;
         ocptr++;
      }

      /**********************/
      /* end character loop */
      /**********************/

      if (eptr == Line)
      {
         /**************/
         /* blank line */
         /**************/

         PreBlankLineCount++;
      }
      else
      {
         /******************/
         /* non-blank line */
         /******************/

         /* go back to the last character copied, terminate line after that */
         lptr = eptr;
         *lptr++ = '\n';
         *lptr = '\0';
         LineLength = lptr - Line;

         if (DoMassageOutput)
            MassageOutputLine (Line, &PreBlankLineCount, &PostBlankLineCount);
         else
         {
            /* replace all of the substituted symbol characters */
            for (lptr = Line; *lptr; lptr++)
               if (*lptr == INTERIM_NON_ASCII_CHAR) *lptr = FINAL_NON_ASCII_CHAR;
         }

         if (PreBlankLineCount)
         {
            if (DoMassageOutput)
            {
               /* ensure multitudes of blank lines are not output */
               if (PreBlankLineCount > MAX_CONSECUTIVE_BLANK_LINES) 
                  PreBlankLineCount = MAX_CONSECUTIVE_BLANK_LINES;

               /* absorb any page-leading blank lines, output others */
               if (LinesOutputToClient)
                  while (PreBlankLineCount--)
                  {
                     TBW += fprintf (stdout, "\n");
                     LinesOutputToClient++;
                  }
               else;
            }
            else
            {
               /* when not massaging the text output all blank lines */
               while (PreBlankLineCount--)
               {
                  TBW += fprintf (stdout, "\n");
                  LinesOutputToClient++;
               }
            }
            PreBlankLineCount = 0;
         }

         TBW += fprintf (stdout, "%s", Line);
         LinesOutputToClient++;

         /* post-blank-lines are only created by the massage function */
         if (PostBlankLineCount)
         {
            PreBlankLineCount = PostBlankLineCount;
            PostBlankLineCount = 0;
         }
      }
   }

   if (!DoMassageOutput)
      while (PreBlankLineCount--)
      {
         TBW += fprintf (stdout, "\n");
         LinesOutputToClient++;
      }
}

/*****************************************************************************/
/*
"Massage" the string of characters constructed from the matrix of
'OutCharStruct's   before it is output to the client.  This "massage" attempts
to make certain  corrections to enhance the layout of the text, these are
clearly indicated and  described within the function.
*/ 

MassageOutputLine
(
char *Line,
int *PreBlankLineCountPtr,
int *PostBlankLineCountPtr
)
{
   static char  LastCharOfPreviousLine = '\0';

   char  *cptr, *lptr;

   /*********/
   /* begin */
   /*********/

   if (Debug) fprintf (stdout, "MassageOutputLine() |%s|\n", Line);

   if (!Line)
   {
      /* initialize */
      LastCharOfPreviousLine = '\0';
      return;
   }

   /**************************/
   /* blank line suppression */
   /**************************/

   if (*PreBlankLineCountPtr == 1)
   {
      /*
         There is one blank line immediately before this line.
         Check to see if this line begins with a lower-case
         alphabetic.  If it does then it is probable (though
         not certain) that the blank line was inserted by
         bit-to-character mapping inconsistancy.  If it is
         lower-case then absorb the blank line.  Step over tags.
      */
      lptr = Line;
      while (*lptr == ' ') lptr++;
      while (*lptr == '<')
      {
         while (*lptr && *lptr != '>') lptr++;
         if (*lptr) lptr++;
         while (*lptr == ' ') lptr++;
      }
      if (*lptr == '(')
         *PreBlankLineCountPtr = 0;
      else
      if (islower(*lptr) &&
          (!ispunct(LastCharOfPreviousLine) ||
           LastCharOfPreviousLine == '-' ||
           LastCharOfPreviousLine == '_' ||
           LastCharOfPreviousLine == ',' ||
           LastCharOfPreviousLine == ';'))
         *PreBlankLineCountPtr = 0;
   }

   /*******************************/
   /* bulleted and numbered lists */
   /*******************************/

   for (cptr = lptr = Line; *lptr; lptr++)
   {
      if (cptr == Line && *lptr == NON_SUPPORTED_CHAR)
      {
         /*
            Symbol characters tend to be used for the "bullets" in lists.
            In this case they would be the first character on the line
            and followed by a space character.  Ensure that the line is
            preceded by at least one blank line for "good" spacing.  If 'cptr'
            equals the start of the line then no non-space characters have
            been encountered.
         */
         if (cptr == Line && *(lptr+1) == ' ' && !*PreBlankLineCountPtr)
            *PreBlankLineCountPtr += 1;
         *lptr = FINAL_NON_ASCII_CHAR;
      }
      else 
      if (cptr == Line && isdigit(*lptr))
      {
         /*
            Numbered lists and headings (at least section headings begin
            a line with (for example) "1. ", "1.1 ", etc.  Detect this and
            ensure the line is preceded by at least one blank line for "good"
            spacing.  If 'cptr' equals the start of the line then no
            non-space characters have been encountered.
         */
         while (isdigit(*lptr)) lptr++;
         if (*lptr == '.')
         {
            while (isdigit(*lptr) || *lptr == '.') lptr++;
            if (*lptr == ' ') *PreBlankLineCountPtr += 1;
         }
      }
      /* note the presence of a non-space character */
      if (*lptr != ' ') cptr = lptr + 1;
   }

   /***********************************************/
   /* take note of the last character on the line */
   /***********************************************/

   cptr = lptr = Line;
   while (*lptr)
      if (*lptr != ' ' && *lptr != '\n')
         cptr = lptr++;
      else
         lptr++;
   LastCharOfPreviousLine = *cptr;
}

/*****************************************************************************/
/*
Creates a "page" in-memory.  The page comprises 1..n of the structure 
'OutCharStruct'.  The macro OUT_CHAR_AT() allows this linear array of
structures to  be addressed with a line/column value as if it was a matrix. 
This essentially  allows it to be treated as a matrix corresponding to a page
of  'MAX_OUTLINE_LENGTH' characters wide and 'NewLinesOnPage' length.  To
increase  efficiency, more than the requested 'NewLinesOnPage' can be created,
reducing  the number of dynamic memory (re)allocations required.
*/ 

PageSize (int NewLinesOnPage)

{
   int  ccnt, status,
        Count,
        BytesToAllocate;
   struct OutCharStruct  *ocptr;

   /*********/
   /* begin */
   /*********/

   if (Debug)
      fprintf (stdout, "PageSize() %d %d/%d\n",
               LastLineOnPage, LinesOnPage, NewLinesOnPage);

   if (NewLinesOnPage < 0)
   {
      LastLineOnPage = 0;
      if (!LinesOnPage) return;
      if (OutPtr)
      {
         for (Count = 0; Count <= LinesOnPage; Count++)
         {
            if (Debug) fprintf (stdout, "Count: %d\n", Count);
            ocptr = OUT_CHAR_AT(Count,0);
            for (ccnt = 0; ccnt < MAX_OUTLINE_LENGTH; ccnt++)
            {
               if (ocptr->htptr) free (ocptr->htptr);
               ocptr++;
            }
         }
      }
      LinesOnPage = 0;
      if (OutPtr) free (OutPtr);
      OutPtr = NULL;
      return;
   }

   if (NewLinesOnPage <= LinesOnPage && OutPtr) return;

   /*
      If the number of lines on the page exceeds a certain threshold assume
      its going to be a large page with many lines and pre-allocate some.
      If it looks like being a very big page, increase preallocation.
   */
   if (LinesOnPage > LINES_ON_PAGE_PREALLOCATE * 30)
      NewLinesOnPage += LINES_ON_PAGE_PREALLOCATE * 10;
   else
   if (LinesOnPage > LINES_ON_PAGE_PREALLOCATE * 15)
      NewLinesOnPage += LINES_ON_PAGE_PREALLOCATE * 5;
   else
   if (LinesOnPage > LINES_ON_PAGE_PREALLOCATE * 5)
      NewLinesOnPage += LINES_ON_PAGE_PREALLOCATE * 2;
   else
      NewLinesOnPage += LINES_ON_PAGE_PREALLOCATE;

   BytesToAllocate = (NewLinesOnPage+1)*MAX_OUTLINE_LENGTH*sizeof(struct OutCharStruct);
   if (!OutPtr)
      OutPtr = calloc (1, BytesToAllocate);
   else
      OutPtr = realloc (OutPtr, BytesToAllocate);
   if (!OutPtr)
   {
      char String [256];

      status = vaxc$errno;
      sprintf (String, ErrorCallocBytes,
         (NewLinesOnPage+1)*MAX_OUTLINE_LENGTH*sizeof(struct OutCharStruct));
      CgiLibResponseError (FI_LI, status, String);

      /* definitely exit if we can't get memory!!! */
      exit (SS$_NORMAL);
   }
   DynamicMemoryAllocated += BytesToAllocate;

   /* scan through all added "lines" */
   if (!LinesOnPage) LinesOnPage = -1;
   while (LinesOnPage < NewLinesOnPage)
   {
      LinesOnPage++;
      ocptr = OUT_CHAR_AT(LinesOnPage,0);
      /* scan through all "characters" on that "line" */
      for (ccnt = 0; ccnt < MAX_OUTLINE_LENGTH; ccnt++)
      {
         /* put a space into the character member, NULL the HTML pointer */
         ocptr->c = ' ';
         ocptr->htptr = NULL;
         ocptr++;
      }
   }
}

/*****************************************************************************/
/*
Dynamically allocate memory for a string.  If there is no existing string then 
allocate the specified size.  If there is an existing string pointed to 
allocate enough for the existing string plus the specified size.  Return a 
pointer to the dynamically allocated string.
*/ 

char* HtmlString
(
char *HtmlStringPtr,
int StringSize
)
{
   int  status;

   /*********/
   /* begin */
   /*********/

   if (!HtmlStringPtr)
   {
      StringSize++;
      if (Debug) fprintf (stdout, "HtmlString() NULL %d\n", StringSize);
      StringSize = ((StringSize / MIN_HTML_STRING_ALLOC) + 1) *
                   MIN_HTML_STRING_ALLOC;
      if (Debug) fprintf (stdout, "StringSize: %d\n", StringSize);
      HtmlStringPtr = calloc (1, StringSize);
      *HtmlStringPtr = '\0';
   }
   else
   {
      StringSize += strlen(HtmlStringPtr);
      if (Debug)
         fprintf (stdout, "HtmlString() |%s| %d\n",
                  HtmlStringPtr, StringSize);
      StringSize = ((StringSize / MIN_HTML_STRING_ALLOC) + 1) *
                   MIN_HTML_STRING_ALLOC;
      if (Debug) fprintf (stdout, "StringSize: %d\n", StringSize);
      HtmlStringPtr = realloc (HtmlStringPtr, StringSize);
   }
   if (!HtmlStringPtr)
   {
      char String [256];

      status = vaxc$errno;
      sprintf (String, ErrorCallocBytes, StringSize);
      CgiLibResponseError (FI_LI, status, String);

      /* definitely exit if we can't get memory!!! */
      exit (SS$_NORMAL);
   }
   DynamicMemoryAllocated += StringSize;
   return (HtmlStringPtr);
}

/*****************************************************************************/
/*
This function comes from original code, so I'm a bit hazy on its functionality.
*/

ProcessChunk ()

{
   /*********/
   /* begin */
   /*********/

   if (Debug) fprintf (stdout, "ProcessChunk()\n");

   if (DebugChunks)
   {
      fprintf (stdout, "ChunkType: %d\n", CurrentChunkType);
      fprintf (stdout, "ChunkLength: %ld\n", CurrentChunkLength);
   }

   if (CurrentChunkType == 3)
   {
      /*****************/
      /* document text */
      /*****************/

      ProcessDocument ();
      return;
   }
   else
   if (CurrentChunkType == 4)
   {
      /***********************************/
      /* contents, tables, figure, index */
      /***********************************/

      ProcessContentsEtc();
      return;
   }
   else
   {
      /***********/
      /* unknown */
      /***********/

      InsertComment (GlobalHorizontal, GlobalVertical,
         "Sorry.  HyperReader does not recognize the document format.",
         __LINE__);

      ChunkPtr += CurrentChunkLength;
      return;
   }
}

/*****************************************************************************/
/*
Process a series of sections of the document body.  These sections can 
comprise text, graphics, figures, etc.
*/ 

ProcessDocument ()

{
   /*********/
   /* begin */
   /*********/

   if (Debug) fprintf (stdout, "ProcessDocument()\n");

   PreviousChunk = *(long*)(ChunkBufferPtr+6);
   SectionsInChunk = *(long*)(ChunkBufferPtr+14);
   PreviousTopicChunk = *(long*)(ChunkBufferPtr+18);
   NextTopicChunk = *(long*)(ChunkBufferPtr+22);

   if (DebugChunks)
   {
      DumpLongWords ("DOCUMENT BODY:", ChunkPtr+2, 6);
      fprintf (stdout, "Chunk: %d\n", CurrentChunk);
      fprintf (stdout, "ChunkType: %d\n", CurrentChunkType);
      fprintf (stdout, "ChunkLength: %d\n", CurrentChunkLength);
      fprintf (stdout, "PreviousChunk: %d\n", PreviousChunk);
      fprintf (stdout, "SectionsInChunk: %d\n", SectionsInChunk);
      fprintf (stdout, "PreviousTopicChunk %d\n", PreviousTopicChunk);
      fprintf (stdout, "NextTopicChunk: %d\n", NextTopicChunk);
   }

   ChunkPtr = ChunkBufferPtr + 26;
   while (ChunkPtr < EndChunkPtr) ProcessSection ();
}

/*****************************************************************************/
/*
Process a single section of the document body.  A section can comprise 
text, graphics, figures, etc.
*/ 

ProcessSection ()

{
   unsigned long  SectionHorizOffset,
                  SectionHorizUnits,
                  SectionLength,
                  SectionSubType,
                  SectionVertOffset,
                  SectionVertUnits;
   unsigned short  SectionType;
   unsigned char  *EndSectionChunkPtr;

   /*********/
   /* begin */
   /*********/

   if (Debug) fprintf (stdout, "ProcessSection()\n");

   SectionType = *(unsigned short*)ChunkPtr;
   SectionSubType = *(unsigned long*)(ChunkPtr+6);
   SectionLength = *(unsigned long*)(ChunkPtr+2);
   SectionHorizOffset = *(unsigned long*)(ChunkPtr+18);
   SectionVertOffset = *(unsigned long*)(ChunkPtr+22);
   SectionHorizUnits = *(unsigned long*)(ChunkPtr+26);
   SectionVertUnits = *(unsigned long*)(ChunkPtr+30);

   EndSectionChunkPtr = ChunkPtr + SectionLength;

   if (Debug)
   {
      fprintf (stdout, "\nSectionType: %d\n", SectionType);
      fprintf (stdout, "SectionSubType: %d\n", SectionSubType);
      fprintf (stdout, "SectionLength: %d\n", SectionLength);
      fprintf (stdout, "SectionHorizOffset: %d\n", SectionHorizOffset);
      fprintf (stdout, "SectionVertOffset: %d\n", SectionVertOffset);
      fprintf (stdout, "SectionHorizUnits: %d\n", SectionHorizUnits);
      fprintf (stdout, "SectionVertUnits: %d\n", SectionVertUnits);
   }

   if (SectionType == 18 || SectionType == 19)
   {
      /********************/
      /* multi-functional */
      /********************/

      if (SectionType == 18)
      {
         /* adjust the vertical origin for all parts in this section */
         GlobalVertical = EndGlobalVertical;
         EndGlobalVertical += SectionVertUnits;
      }

      if (DebugSections)
      {
         DumpLongWords ("SECTION 18:", ChunkPtr+2, 10);
         fprintf (stdout, "ChunkPtr: %d\n", ChunkPtr);
         fprintf (stdout, "next ChunkPtr %d\n", ChunkPtr+SectionLength);
      }

      if (SectionSubType == 2)
      {
         /********/
         /* text */
         /********/

         ChunkPtr += 42;
         while (ChunkPtr < EndSectionChunkPtr)
            if (!ProcessTextSection (GlobalHorizontal, GlobalVertical,
                                     NULL, NULL))
               break;
         ChunkPtr = EndSectionChunkPtr;
      }
      else
      if (SectionSubType == 3)
      {
         /****************************************/
         /* some sort of X line-drawing language */
         /****************************************/
/*
         fprintf (stdout, "\n%s\n", ChunkPtr+42); 
         DumpBytes ("Section 18 SubType 3", ChunkPtr+42, SectionLength-42);
*/
         InsertComment (GlobalHorizontal, GlobalVertical,
            "Sorry.  HyperReader cannot reproduce this figure.",
            __LINE__);

         ChunkPtr = EndSectionChunkPtr;
      }
      else
      if (SectionSubType == 4 || SectionSubType == 5)
      {
         /****************/
         /* bitmap image */
         /****************/

         PlaceImage (GlobalHorizontal+SectionHorizOffset, GlobalVertical);
      }
      else
      {
         /***********/
         /* unknown */
         /***********/

         InsertComment (GlobalHorizontal, GlobalVertical,
            "Sorry.  HyperReader does not recognize the document format.",
            __LINE__);

         ChunkPtr = EndSectionChunkPtr;
      }

      return;
   }
   else
   if (SectionType == 20 || SectionType == 21)
   {
      /***********/
      /* hotspot */
      /***********/

      ProcessHotSpot (GlobalHorizontal, GlobalVertical);
      return;
   }
   else
   if (SectionType == 22 || SectionType == 23)
   {
      /*******************/
      /* graphic overlay */
      /*******************/

      ProcessGraphicOverlay ();
      return;
   }
   else
   {
      /***********/
      /* unknown */
      /***********/

      InsertComment (GlobalHorizontal, GlobalVertical,
         "Sorry.  HyperReader does not recognize the document format.",
         __LINE__);

      ChunkPtr = EndSectionChunkPtr;
      return;
   }
}

/*****************************************************************************/
/*
Process a chunk containing a table of contents, tables, figures, or an index.
Strictly control the line and column placement according to what we want, 
don't pay attention to any information associated with the text.
*/

ProcessContentsEtc ()

{
   int  Count;
   unsigned short  DataType;
   unsigned long  DataLength,
                  HotChunkNumber,
                  Column,
                  Line = 0,
                  Horizontal,
                  Vertical,
                  Length,
                  Width;
   char  NestedLevel;
   unsigned char  *NextChunkPtr,
                  *chptr;
   struct OutCharStruct  *ocptr;

   /*********/
   /* begin */
   /*********/

   if (Debug) fprintf (stdout, "ProcessContentsEtc()\n");

   EnhanceText = false;
   ChunkPtr = ChunkBufferPtr + 0x06;

   while (ChunkPtr < EndChunkPtr)
   {
      DataType = *(unsigned short*)ChunkPtr;
      DataLength = *(unsigned long*)(ChunkPtr+2);
      NestedLevel = *(ChunkPtr+6);

      if (DebugChunks)
      {
         DumpLongWords ("CONTENTS:", ChunkPtr, 5);
         fprintf (stdout, "DataType: %d\n", DataType);
         fprintf (stdout, "DataLength: %d\n", DataLength);
         fprintf (stdout, "NestedLevel: %d\n", NestedLevel);
      }

      NextChunkPtr = ChunkPtr + DataLength;
      chptr = ChunkPtr + 20;

      while (*chptr == 2 || *chptr == 3) chptr += (unsigned char)*(chptr+1);
      while (*chptr++);

      /* I'm buggered if I know how this next statement decides! */
      if (chptr == NextChunkPtr-4)
         HotChunkNumber = *(unsigned long*)chptr;
      else
         HotChunkNumber = 0;

      ChunkPtr += 20;

      /* on all but the first line add a blank line before level 1 items */
      if (Line++)
         if (NestedLevel == 1) Line++;

      /* indent the lines of text according to the nesting level */
      Column = 0;
      for (Count = NestedLevel * 2; Count; Count--) Column++;

      while (*ChunkPtr == 3 || *ChunkPtr == 2)
      {
         Column = PlaceText (Line, Column);
         Column++;
      }
      Column--;

      if (HotChunkNumber)
      {
         Horizontal = SkipContentsItemNumber (Line) * COLUMN_POSITIONING_UNITS;
         Vertical = Line * LINE_POSITIONING_UNITS;
         Length = (Column * COLUMN_POSITIONING_UNITS) - Horizontal;
         Width = LINE_POSITIONING_UNITS;
         PlaceHotSpot (0, 0, Horizontal, Vertical,
                       Length, Width, HotChunkNumber);
      }

      /* scan past null-terminated text */
      while (*ChunkPtr++);

      ChunkPtr = NextChunkPtr;
   }
}

/*****************************************************************************/
/*
Return the column position of the first character following an item number 
such as "1.", "1.2", "1.2.3", "1-2", "1-2-3", "A.", "A.1", "A-1", "REF-1", 
etc.  If the line does not begin with such a string then return the column 
position of the first non-space character.  This allows HTML links to start on 
the description rather than item number of a contents line.
*/ 

int SkipContentsItemNumber (int Line)

{
   int  Column,
        FirstNonSpaceColumn;
   char  ccnt;
   struct OutCharStruct  *ocptr;

   /*********/
   /* begin */
   /*********/

   if (Debug) fprintf (stdout, "SkipContentsItemNumber()\n");

   Column = 0;
   ocptr = OUT_CHAR_AT(Line,Column);

   /* skip leading white-space */
   while (Column < MAX_OUTLINE_LENGTH && ocptr->c == ' ')
   {
      ocptr++;
      Column++;
   }
   if (Column >= MAX_OUTLINE_LENGTH) return (0);
   FirstNonSpaceColumn = Column;

   if (isdigit(ocptr->c))
   {
      /*
         Line starts with a digit.  Scan across anything like
         a heading number, e.g. "1", "1.1", "1-1", etc.
      */
      while (Column < MAX_OUTLINE_LENGTH && 
             (isalnum(ocptr->c) || ocptr->c == '.' || ocptr->c == '-'))
      {
         ocptr++;
         Column++;
      }
      if (Column >= MAX_OUTLINE_LENGTH)
         return (FirstNonSpaceColumn);
      else;
   }
   else
   if (isalpha(ocptr->c))
   {
      /*
         Line starts with an alphabetic.  Check if its a single
         alphabetic such an Appendix (e.g. "A Topic"), or an alphabetic
         with sub-numbering (e.g. "A-1 Topic", "REF-1 Topic").
      */
      ocptr++;
      Column++;
      if (Column >= MAX_OUTLINE_LENGTH) return (FirstNonSpaceColumn);
      /* so far its been a single alphabetic, check if followed by a space */
      if (ocptr->c != ' ')
      {
         /* not a space, step over any contiguous alphas or digits */
         while (Column < MAX_OUTLINE_LENGTH &&
                (isalpha(ocptr->c) || isdigit(ocptr->c)))
         {
            ocptr++;
            Column++;
         }
         if (Column >= MAX_OUTLINE_LENGTH) return (FirstNonSpaceColumn);
         /* if its not one of these two then its not a contents-"number" */
         if (!(ocptr->c == '.' || ocptr->c == '-'))
            return (FirstNonSpaceColumn);
         /* step over any following alphas, digits, "." or "-" */
         while (Column < MAX_OUTLINE_LENGTH &&
                (isalpha(ocptr->c) || isdigit(ocptr->c) ||
                 ocptr->c == '.' || ocptr->c == '-'))
         {
            ocptr++;
            Column++;
         }
         if (Column >= MAX_OUTLINE_LENGTH)
            return (FirstNonSpaceColumn);
         else;
      }
      else;
   }
   else
      return (FirstNonSpaceColumn);

   /* scan across the intervening white-space */
   while (Column < MAX_OUTLINE_LENGTH && ocptr->c == ' ')
   {
      ocptr++;
      Column++;
   }
   if (Column >= MAX_OUTLINE_LENGTH) return (FirstNonSpaceColumn);
   return (Column);
}

/*****************************************************************************/
/*
Process a section of document text.  Locate it from the horizontal and 
vertical origins provided.
*/

ProcessTextSection
(
unsigned long H_Origin,
unsigned long V_Origin
)
{
   int  Line,
        Column;
   long  DataLength;
   unsigned short  Horizontal;
   unsigned short  Vertical;

   /*********/
   /* begin */
   /*********/

   if (Debug) 
      fprintf (stdout, "ProcessTextSection() H_Origin: %d V_Origin: %d\n",
               H_Origin, V_Origin);

   if (*ChunkPtr == 1)
   {
      /***********/
      /* graphic */
      /***********/

      ProcessGraphicLine ();
      return (true);
   }
   else
   if (*ChunkPtr == 2 || *ChunkPtr == 3)
   {
      /***************/
      /* actual text */
      /***************/

      Horizontal = *(unsigned short*)(ChunkPtr+2);
      Vertical = *(unsigned short*)(ChunkPtr+4);

      Line =  (V_Origin + Vertical) / LINE_POSITIONING_UNITS;
      Column = (H_Origin + Horizontal) / COLUMN_POSITIONING_UNITS;

      PlaceText (Line, Column);

      return (true);
   }
   else
   {
      /***********/
      /* unknown */
      /***********/

      InsertComment (H_Origin, V_Origin,
         "Sorry.  HyperReader does not recognize the document format.",
         __LINE__);

      return (false);
   }
}

/*****************************************************************************/
/*
These seem to do things like draw boxes around SDML <BOX>() text and 
horizontal lines (e.g. the lines delimiting <NOTE>s) etc.  It looks like a 
single line drawing functionality.  These are ignored for HTML.
*/ 

ProcessGraphicLine ()

{
   long  DataLength;

   /*********/
   /* begin */
   /*********/

   if (Debug) fprintf (stdout, "ProcessGraphicLine()\n");

   DataLength = *(unsigned char*)(ChunkPtr+1);

   if (DebugGraphics)
   {
      fprintf (stdout,"DataLength: %08.08X (%d)\n", DataLength, DataLength);
      DumpLongWords ("GRAPHIC LINE:", ChunkPtr+2, (DataLength-2)/4);
   }

   ChunkPtr += DataLength;
}

/*****************************************************************************/
/*
These seem to do things like place a block of shading (stipple) over a section 
of text.  It must be some sort of graphical "block" functionality.  The data 
structure appears similar to figures (images).  These are ignored for HTML.
*/ 

ProcessGraphicOverlay ()

{
   long  DataLength;

   /*********/
   /* begin */
   /*********/

   if (Debug) fprintf (stdout, "ProcessGraphicOverlay()\n");

   DataLength = *(long*)(ChunkPtr+2);

   if (DebugGraphics)
   {
      fprintf (stdout,"DataLength: %08.08X (%d)\n", DataLength, DataLength);
      DumpLongWords ("GRAPHIC OVERLAY:", ChunkPtr+2, (DataLength-2)/4);
   }

   ChunkPtr += DataLength;
}

/*****************************************************************************/
/*
Create a hotspot by placing HTML link(s) around text in the specified 
location(s).  Handles single and two-line-spanning hotspots.
*/

ProcessHotSpot
(
unsigned long H_Origin,
unsigned long V_Origin
)
{
   unsigned char  *chptr;
   unsigned long  DataLength,
                  Horizontal,
                  Vertical,
                  Length,
                  Width,
                  HotChunkNumber,
                  GraphicCount,
                  SpanningDataLength;

   /*********/
   /* begin */
   /*********/

   if (Debug) fprintf (stdout, "ProcessHotSpot()\n");

   DataLength = *(unsigned long*)(ChunkPtr+2);
   HotChunkNumber = *(unsigned long*)(ChunkPtr+34);
   SpanningDataLength = *(unsigned long*)(ChunkPtr+38);

   if (DebugHotspots)
   {
      DumpLongWords ("HOTSPOT:", ChunkPtr+2, 10);
      fprintf (stdout,"DataLength: %d\n", DataLength);
      fprintf (stdout,"HotChunkNumber: %d\n", HotChunkNumber);
      fprintf (stdout,"SpanningDataLength: %d\n", SpanningDataLength);
      if (SpanningDataLength)
         DumpLongWords ("SPANNING HOTSPOT:", ChunkPtr+42,
                        SpanningDataLength / sizeof(long));
   }

   if (SpanningDataLength)
   {
      /*
         "Spanning hotspots" are generated when a hotspot spans more
         than one line.  Bookreader seems to draw a series of graphic 
         lines starting with the top text line, bottom left, up and
         around and then down to the next line, and around that text.
         Emulate this by drawing two hotspots.
         If the hotspot spans three lines?  Tough!
      */

      chptr = ChunkPtr + 42;

      GraphicCount = *(unsigned long*)chptr;
      if (DebugHotspots) fprintf (stdout,"GraphicCount: %d\n", GraphicCount);

      chptr += 4;
      Horizontal = *(unsigned long*)(chptr+8);
      Vertical = *(unsigned long*)(chptr+12);
      Length = *(unsigned long*)(chptr+16) - Horizontal;
      Width = *(unsigned long*)(chptr+4) - Vertical;
      PlaceHotSpot (H_Origin, V_Origin, Horizontal, Vertical,
                    Length, Width, HotChunkNumber);

      chptr += 32;
      Horizontal = *(unsigned long*)(chptr+16);
      Vertical = *(unsigned long*)(chptr+28);
      Length = *(unsigned long*)chptr - Horizontal;
      Width = *(unsigned long*)(chptr+20) - Vertical;
      PlaceHotSpot (H_Origin, V_Origin, Horizontal, Vertical,
                    Length, Width, HotChunkNumber);
   }
   else
   {
      /* single line hotspot */

      Horizontal = *(unsigned long*)(ChunkPtr+18);
      Vertical = *(unsigned long*)(ChunkPtr+22);
      Length = *(unsigned long*)(ChunkPtr+26);
      Width = *(unsigned long*)(ChunkPtr+30);

      PlaceHotSpot (H_Origin, V_Origin, Horizontal, Vertical,
                    Length, Width, HotChunkNumber);
   }

   ChunkPtr += DataLength;
}

/*****************************************************************************/
/*
Create a hotspot by placing the start of an HTML link at the line and column 
represented by horizontal/vertical, and then placing the end of the link at 
horizontal+length/vertical+width.
*/

PlaceHotSpot
(
unsigned long H_Origin,
unsigned long V_Origin,
unsigned long Horizontal,
unsigned long Vertical,
unsigned long Length,
unsigned long Width,
unsigned long HotChunkNumber
)
{
   int  ccnt,
        StringLength,
        Line,
        Column;
   char  String [1024];
   struct OutCharStruct  *ocptr;

   /*********/
   /* begin */
   /*********/

   if (Debug) fprintf (stdout, "PlaceHotSpot()\n");

   if (DebugHotspots)
   {
      fprintf (stdout,"H_Origin: %d\n", H_Origin);
      fprintf (stdout,"V_Origin: %d\n", V_Origin);
      fprintf (stdout,"Horizontal: %d\n", Horizontal);
      fprintf (stdout,"Vertical: %d\n", Vertical);
      fprintf (stdout,"Length: %d\n", Length);
      fprintf (stdout,"Width: %d\n", Width);
      fprintf (stdout,"HotChunkNumber: %d\n", HotChunkNumber);
   }

   /* 
       Attempt to place this on the line containing the text to be 
       "hotspot"ed.  'Vert' is the vertical location,  'Width' must
       be the the width of the hotspot area.  Three-quarters down
       the 'Width' value is used to calculate the line position.

       BTW; seems to work well ... most of the time!

       Use bit-wise shifting to enhance the speed of the divisions:
       excerpt equals: "(((Vert + (Width / 2) + (Width / 4))"
   */

   Line = (V_Origin + Vertical + (Width >> 1) + (Width >> 2))
          / LINE_POSITIONING_UNITS;
   if (Line > LinesOnPage) PageSize (Line);
   if (Line > LastLineOnPage) LastLineOnPage = Line;

   /*
      If this is a totally blank line then its probably an artifact of the
      mapping process.  Attempt a correction by stepping to the next line.
   */
   for (;;)
   {
      ocptr = OUT_CHAR_AT(Line,0);
      for (ccnt = 0; ccnt < MAX_OUTLINE_LENGTH; ccnt++)
      {
         if (ocptr->c != ' ') break;
         ocptr++;
      }
      if (ccnt < MAX_OUTLINE_LENGTH || Line >= LinesOnPage) break;
      Line++;
   }

   /*
      Calculate the hotspot start column.  If it lands on a space then
      scan forward to the first non-space character encountered.
   */

   Column = (H_Origin + Horizontal) / COLUMN_POSITIONING_UNITS;
   if (Column >= MAX_OUTLINE_LENGTH) Column = MAX_OUTLINE_LENGTH-1;

   ocptr = OUT_CHAR_AT(Line,Column);
   for (ccnt = Column; ccnt < MAX_OUTLINE_LENGTH; ccnt++)
   {
      if (ocptr->c != ' ') break;
      ocptr++;
   }
   if (ccnt < MAX_OUTLINE_LENGTH) Column = ccnt;

   /* place the start of hotspot into the text */

   StringLength = sprintf (String,
      "<a href=\"%s%s?%schunk=%d&referer=%s&title=%s%s\">",
      CgiScriptNamePtr, UrlEncodedCgiPathInfoPtr, BookSpecifiedByFile,
      (int)SectionChunkNumbersArrayPtr[HotChunkNumber],
      UrlEncReferer, UrlEncTitle, UrlDoMassage);

   ocptr = OUT_CHAR_AT(Line,Column);
   ocptr->htptr = HtmlString (ocptr->htptr, StringLength);
   strcat (ocptr->htptr, String);

   /*
      Calculate the hotspot end column.  If it lands on a space then
      scan backward to the first non-space character encountered.
   */

   if (Column + Length / COLUMN_POSITIONING_UNITS <= Column)
   {
      Column++;
      if (Column >= MAX_OUTLINE_LENGTH) Column = MAX_OUTLINE_LENGTH-1;
   }
   else
   {
      Column += (Length / COLUMN_POSITIONING_UNITS) - 1;
      if (Column >= MAX_OUTLINE_LENGTH) Column = MAX_OUTLINE_LENGTH-1;

      ocptr = OUT_CHAR_AT(Line,Column);
      for (ccnt = Column; ccnt > 0; ccnt--)
      {
         if (ocptr->c != ' ') break;
         ocptr--;
      }
      if (ccnt > 0) Column = ccnt + 1;
      if (Column >= MAX_OUTLINE_LENGTH) Column = MAX_OUTLINE_LENGTH-1;
   }

   /* place the end of hotspot into the text */
   ocptr = OUT_CHAR_AT(Line,Column);
   ocptr->htptr = HtmlString (ocptr->htptr, 4);
   strcat (ocptr->htptr, "</A>");
}

/*****************************************************************************/
/*
Place a "string" of text characters into the "page".  With Bookreader 
documents a single "string" of characters NEVER spans multiple lines.  The 
"string" is pointed to by 'ChunkPtr'.
*/

int PlaceText
(
int Line,
int Column
)
{
   static int  CallCount = 0;

   int  idx, count,
        FontNumber;
   unsigned char  TextType;
   unsigned char  Length;
   unsigned short  Horizontal;
   unsigned short  Vertical;
   unsigned short  Unknown;
   unsigned char  *Text;
   struct OutCharStruct  *ocptr;

   /*********/
   /* begin */
   /*********/

   if (Line > LinesOnPage) PageSize (Line);
   if (Line > LastLineOnPage) LastLineOnPage = Line;
   if (Column >= MAX_OUTLINE_LENGTH) Column = MAX_OUTLINE_LENGTH-1;

   TextType = *ChunkPtr;
   Length = *(ChunkPtr+1);
   FontNumber = *(ChunkPtr+6);
   Unknown = *(unsigned short*)(ChunkPtr+7);

   if (DebugText)
   {
      fprintf (stdout,
"\nPlaceText()\n\
Len: %d  ?: %d  Line: %d  Column: %d  Font: %d  Bld: %d  It: %d  Sym: %d\n",
      Length, Unknown, Line, Column,
      FontNumber,
      FontArrayPtr[FontNumber].FontBold,
      FontArrayPtr[FontNumber].FontItalic,
      FontArrayPtr[FontNumber].FontUnsupported);
   }

   /********************/
   /* text enhancement */
   /********************/

   if (EnhanceText)
   {
      if (FontArrayPtr[FontNumber].FontBold)
      {
         ocptr = OUT_CHAR_AT(Line,Column);
         ocptr->htptr = HtmlString (ocptr->htptr, 3);
         strcat (ocptr->htptr, "<b>");
      }
      else
      if (FontArrayPtr[FontNumber].FontItalic)
      {
         ocptr = OUT_CHAR_AT(Line,Column);
         ocptr->htptr = HtmlString (ocptr->htptr, 3);
         strcat (ocptr->htptr, "<i>");
      }
   }

   /*********************************************/
   /* put the characters into the output buffer */
   /*********************************************/

   Text = ChunkPtr+9;
   idx = 0;
   while (idx < Length-8)
   {
      count = (int)Text[idx++];
      while (count-- > 0)
      {
         if (Column < MAX_OUTLINE_LENGTH)
         {
            if (FontArrayPtr[FontNumber].FontUnsupported ||
                Text[idx] < 0x20 ||
                (Text[idx] > 0x7f && Text[idx] < 0xa0))
               OUT_CHAR_AT(Line,Column)->c = NON_SUPPORTED_CHAR;
            else
               OUT_CHAR_AT(Line,Column)->c = Text[idx];

            if (DebugText)
            {
/*
               fprintf (stdout, "%c", OUT_CHAR_AT(Line,Column)->c);
*/
               fprintf (stdout, "%c(%02.02x)[%d] ",
                       OUT_CHAR_AT(Line,Column)->c,
                       OUT_CHAR_AT(Line,Column)->c,
                       Column);
            }
         }
         Column++;
         idx++;
      }
      if (idx < Length-9)
         Column += (Text[idx] / COLUMN_POSITIONING_UNITS) + 1;
      idx++;
   }

   if (DebugText) fputc ('\n', stdout);
   if (Column >= MAX_OUTLINE_LENGTH) Column = MAX_OUTLINE_LENGTH-1;

   /************************/
   /* end text enhancement */
   /************************/

   if (EnhanceText)
   {
      if (FontArrayPtr[FontNumber].FontBold)
      {
         ocptr = OUT_CHAR_AT(Line,Column);
         ocptr->htptr = HtmlString (ocptr->htptr, 4);
         strcat (ocptr->htptr, "</b>");
      }
      else
      if (FontArrayPtr[FontNumber].FontItalic)
      {
         ocptr = OUT_CHAR_AT(Line,Column);
         ocptr->htptr = HtmlString (ocptr->htptr, 4);
         strcat (ocptr->htptr, "</i>");
      }
   }

   ChunkPtr += Length;
   return (Column);
}

/*****************************************************************************/
/*
Put a link into the text to retrieve the image.
*/

PlaceImage
(
unsigned long H_Origin,
unsigned long V_Origin
)
{
   int  ccnt,
         Length,
        Line,
        Column;
   long  DataLength,
         Horizontal,
         Offset,
         Vertical;
   char  String [2048];
   struct OutCharStruct  *ocptr;

   /*********/
   /* begin */
   /*********/

   if (Debug)
      fprintf (stdout, "PlaceImage() H_Origin: %d V_Origin: %d\n",
               H_Origin, V_Origin);

   DataLength = *(long*)(ChunkPtr+2);
   Horizontal = *(long*)(ChunkPtr+18);
   Vertical = *(long*)(ChunkPtr+22);

   if (Debug)
   {
      DumpLongWords ("FIGURE:", ChunkPtr+2, 12);
      fprintf (stdout,"DataLength : %d\n", DataLength);
      fprintf (stdout,"Horizontal : %d\n", Horizontal);
      fprintf (stdout,"Vertical : %d\n", Vertical);
   }

   Line = (V_Origin + Vertical) / LINE_POSITIONING_UNITS;
   if (!Line) Line = 1;
   if (Line > LinesOnPage) PageSize (Line);
   if (Line > LastLineOnPage) LastLineOnPage = Line;

   /*
      Without the ability to flow text around an image (which current
      browsers lack) and we're on a line containing text attempt to step
      back to a blank line.
   */
   for (;;)
   {
      ocptr = OUT_CHAR_AT(Line,0);
      for (ccnt = 0; ccnt < MAX_OUTLINE_LENGTH; ccnt++)
      {
         if (ocptr->c != ' ') break;
         ocptr++;
      }
      if (ccnt >= MAX_OUTLINE_LENGTH || Line <= 1) break;
      Line--;
   }

   Column = (H_Origin + Horizontal) / COLUMN_POSITIONING_UNITS;
   if (Column >= MAX_OUTLINE_LENGTH) Column = MAX_OUTLINE_LENGTH-1;
   if (DebugFigures) fprintf (stdout,"Line: %d Column: %d\n", Line, Column);

   /*
      The four numbers in the URI are:
      (note that parts one and two constitute the RMS RFA of the record)

      1.  Virtual Block Number of the start of the part
      2.  starting byte position in the VBN
      3.  total length of the chunk
      4.  offset from start of chunk the image data begins

      This will allow the image to be retrieved very quickly by merely
      opening the file, reading the part (one or more records) via the RFA
      and jumping in to process the image data from the offset.
   */

   Offset = ChunkPtr - ChunkBufferPtr;

   Length = sprintf (String,
"<img src=\"%s%s?%sgraphic=%d,%d,%d,%d\" align=\"top\" alt=\"&lt;image&gt;\">\n",
   CgiScriptNamePtr, UrlEncodedCgiPathInfoPtr, BookSpecifiedByFile,
   ChunkArrayPtr[GetChunkNumber].VBN, ChunkArrayPtr[GetChunkNumber].VBNbyte,
   ChunkArrayPtr[GetChunkNumber].Length, Offset);

   ocptr = OUT_CHAR_AT(Line,Column);
   ocptr->htptr = HtmlString (ocptr->htptr, Length);
   strcat (ocptr->htptr, String);

   ChunkPtr += DataLength;
}

/*****************************************************************************/
/*
Read the font information from the respective part of the Bookreader file.  
Font 'numbers' represent the font being specified within bookreader text.  
Note the point value of each font, and which font 'number' is bold, italic, 
symbol, etc.  This information is used when mapping the text.
*/ 

GetFonts ()

{
   int  count,
        FontCount;
   short  Short1,
          Short2,
          Short3,
          FontNumber;
   char  *fptr, *cptr;

   /*********/
   /* begin */
   /*********/

   if (Debug) fprintf (stdout, "GetFonts()\n");

   GetChunk (&FontChunk);
   fptr = (char*)ChunkBufferPtr+6;
   for (FontCount = 0; FontCount < NumberOfFonts; FontCount++)
   {
      Short1 = *(short*)fptr;
      Short2 = *(short*)(fptr+2);
      Short3 = *(short*)(fptr+4);
      FontNumber = *(short*)(fptr+6);

      if (DebugFonts)
         fprintf (stdout, "Font \
#%03d 0x%04.04x  0x%04.04x  0x%04.04x  0x%04.04x\n|%s|\n",
FontNumber, Short1, Short2, Short3, FontNumber, fptr+8);

      FontArrayPtr[FontNumber].FontSize = 0;
      count = 0;
      cptr = (char*)fptr+8;
      while (*cptr && count < 8)
          if (*cptr++ == '-') count++;
      FontArrayPtr[FontNumber].FontSize = atoi (cptr) / 10;

      /* push the font name to upper case for easy strstr() below */
      for (cptr = fptr+8; *cptr; cptr++) *cptr = toupper(*cptr);

      FontArrayPtr[FontNumber].FontBold =
      FontArrayPtr[FontNumber].FontItalic =
      FontArrayPtr[FontNumber].FontUnsupported = false;

      /* "*-interim dm-*" has some interesting graphics/symbol characters */
      if (strstr (fptr+8, "INTERIM DM"))
         FontArrayPtr[FontNumber].FontUnsupported = true;
      else
      if (strstr (fptr+8, "SYMBOL"))
         FontArrayPtr[FontNumber].FontUnsupported = true;
      else
      if (strstr (fptr+8, "BOLD"))
         FontArrayPtr[FontNumber].FontBold = true;
      else
      if (strstr (fptr+8, "-I-"))
         FontArrayPtr[FontNumber].FontItalic = true;

      fptr += 8;
      while (*fptr) fptr++;
      fptr++;
   }
}

/*****************************************************************************/
/*
This function comes from original code, so I'm a bit hazy on its functionality.
*/

GetSectionXref ()

{
   int  idx;
   char  *cptr;

   /*********/
   /* begin */
   /*********/

   if (Debug) fprintf (stdout, "GetSectionXref()\n");

   GetChunk (&SectionXrefChunk);

   cptr = (char*)ChunkBufferPtr + 0x06;
   for (idx = 0; idx < NumberOfSections; idx++)
   {
       SectionChunkNumbersArrayPtr[idx] = *(long*)cptr;
/*
       if (DebugChunks)
          fprintf (stdout, "Content %d.  0x%08.08lx\n", idx, *(long*)cptr);
*/
       cptr += sizeof(long);
   }
}

/*****************************************************************************/
/*
This function comes from original code, so I'm a bit hazy on its functionality.
*/

GetFirstChunk ()

{
   int  status,
        SegmentNameLength;
   short  SegmentType,
          SegmentLength;
   long  SegmentData1,
         SegmentData2,
         SegmentData3,
         SegmentChunkNumber;
   char  *chptr,
         *SegmentNamePtr;

   /*********/
   /* begin */
   /*********/

   if (Debug) fprintf (stdout, "GetFirstChunk()\n");

   CurrentSeek = 0;
   ChunkNumber = 0;

   FirstChunk.VBN = 1;
   FirstChunk.VBNbyte = 0;
   FirstChunk.Length = 1022;
   GetChunk (&FirstChunk);

   NumberOfChunks = *(long*)(ChunkBufferPtr+0x46);
   NumberOfSections = *(long*)(ChunkBufferPtr+0x4a);
   NumberOfFonts = *(long*)(ChunkBufferPtr+0x52);
   LastVBN = *(long*)(ChunkBufferPtr+0x5a);
   LastVBNbyte = *(short*)(ChunkBufferPtr+0x5e);
   LastVBNlen = *(short*)(ChunkBufferPtr+0x60);

   LastChunk.VBN = LastVBN;
   LastChunk.VBNbyte = LastVBNbyte;
   LastChunk.Length = LastVBNlen;

   strcpy (BookTitle, (char*)ChunkBufferPtr+0x7f);

   if (DebugChunks)
   {
      fprintf (stdout, "ChunkType: %04.04X (%d)\n",
               CurrentChunkType, CurrentChunkType);
      fprintf (stdout, "ChunkLength: %08.08X (%ld)\n",
               CurrentChunkLength, CurrentChunkLength);
      fprintf (stdout, "NumberOfChunks: %08.08X (%ld)\n",
              NumberOfChunks, NumberOfChunks);
      fprintf (stdout, "NumberOfSections: %08.08X (%ld)\n",
              NumberOfSections, NumberOfSections);
      fprintf (stdout, "NumberOfFonts: %08.08X (%ld)\n",
              NumberOfFonts, NumberOfFonts);
      fprintf (stdout, "LastVBN: %08.08X (%ld)\n",
               LastVBN, LastVBN);
      fprintf (stdout, "LastVBNbyte: %04.04X (%d)\n",
              (int)LastVBNbyte, (int)LastVBNbyte);
      fprintf (stdout, "LastVBNlen: %04.04X (%d)\n",
              (int)LastVBNlen, (int)LastVBNlen);
      fprintf (stdout, "BookTitle: |%s|\n", BookTitle);
   }

   ChunkArrayPtr = calloc (sizeof(struct ChunkDataStruct), NumberOfChunks);
   if (!ChunkArrayPtr)
   {
      status = vaxc$errno;
      CgiLibResponseError (FI_LI, status, ErrorCalloc);
      /* definitely exit if we can't get memory!!! */
      exit (SS$_NORMAL);
   }

   SectionChunkNumbersArrayPtr = calloc (sizeof(int), NumberOfSections);
   if (!SectionChunkNumbersArrayPtr)
   {
      status = vaxc$errno;
      CgiLibResponseError (FI_LI, status, ErrorCalloc);
      /* definitely exit if we can't get memory!!! */
      exit (SS$_NORMAL);
   }

   FontArrayPtr = calloc (sizeof(struct FontStruct), NumberOfFonts);
   if (!FontArrayPtr)
   {
      status = vaxc$errno;
      CgiLibResponseError (FI_LI, status, ErrorCalloc);
      /* definitely exit if we can't get memory!!! */
      exit (SS$_NORMAL);
   }

   chptr = (char*)ChunkBufferPtr + 0x128;
   while (*(short*)chptr)
   {
      SegmentType = *(short*)chptr;
      SegmentLength = *(short*)(chptr+2);
      SegmentData1 = *(long*)(chptr+4);
      SegmentData2 = *(long*)(chptr+10);
      SegmentData3 = *(long*)(chptr+11);
      SegmentChunkNumber = *(long*)(chptr+15);
      SegmentNamePtr = chptr+20;

      if (Debug && SegmentType == 12)
      {
         fprintf (stdout,
"Segment; Type: %04.04X Length: %04.04X SegmentNamePtr: |%s|\n\
Data1: %08.08X Data2: %08.08X Data3: %08.08X ChunkNumber: %08.08X\n",
            SegmentType, SegmentLength,  SegmentNamePtr,
            SegmentData1, SegmentData2, SegmentData3, SegmentChunkNumber);
      }

      if (SegmentType == 12)
      {
         if (++BookDivisionCount >= MAX_DIVISIONS)
         {
            CgiLibResponseError (FI_LI, 0, ErrorTooManyDivisions);
            return;
         }

         BookDivision[BookDivisionCount].DescriptionPtr =
            calloc (1, SegmentNameLength = strlen(SegmentNamePtr) + 1);
         if (!BookDivision[BookDivisionCount].DescriptionPtr)
         {
            status = vaxc$errno;
            CgiLibResponseError (FI_LI, status, ErrorCalloc);

            /* definitely exit if we can't get memory!!! */
            exit (SS$_NORMAL);
         }
         memcpy (BookDivision[BookDivisionCount].DescriptionPtr,
                 SegmentNamePtr,
                 SegmentNameLength);
         BookDivision[BookDivisionCount].ChunkNumber = SegmentChunkNumber;
      }

      chptr += SegmentLength;
   }
}

/*****************************************************************************/
/*
The last "chunk" contains a series of ten-byte structures containing the 
Record File Address (RFA) of each "chunk" (the VBN and byte of VBN), and the 
total length of the "chunk", which can span individual records.
*/ 

GetLastChunk ()

{
   int  count,
        SeekPos;
   short  ChunkType;
   char  *chptr;

   /*********/
   /* begin */
   /*********/

   if (Debug) fprintf (stdout, "GetLastChunk()\n");

   /* last record/part is the table of parts */
   GetChunk (&LastChunk);

   if (DebugChunks)
       fprintf (stdout,
"(Xref = 2, Headings = 3, Symbols = 4, Fonts = 5)\n\
Chunks \
num   VBN          VBNbyte   len\n");

   chptr = (char*)ChunkBufferPtr + 6;
   for (count = 0; count < NumberOfChunks; count++)
   {
      ChunkArrayPtr[count].VBN = *(long*)chptr;
      ChunkArrayPtr[count].VBNbyte = *(short*)(chptr+4);
      ChunkArrayPtr[count].Length = *(long*)(chptr+6);
      chptr += 10;

/*
      if (DebugChunks)
      {
         fprintf (stdout, "Chunk \
%3d   0x%08.08lx   0x%04.04x   0x%08.08lx\n",
         count, ChunkArrayPtr[count].VBN, (int)ChunkArrayPtr[count].VBNbyte,
         ChunkArrayPtr[count].Length);
      }
*/
   }

   /* >>>>> THE FOLLOWING IS AN AREA FOR POSSIBLE IMPROVEMENT <<<<< */

   for (count = 0; count < NumberOfChunks; count++)
   {
      GetChunk (&ChunkArrayPtr[count]);
      if (CurrentChunkType == 0x0006)
      {
         SectionXrefChunk.VBN = ChunkArrayPtr[count].VBN;
         SectionXrefChunk.VBNbyte = ChunkArrayPtr[count].VBNbyte;
         SectionXrefChunk.Length = ChunkArrayPtr[count].Length;
      }
      else
      if (CurrentChunkType == 0x0007)
      {
         SectionHeadingsChunk.VBN = ChunkArrayPtr[count].VBN;
         SectionHeadingsChunk.VBNbyte = ChunkArrayPtr[count].VBNbyte;
         SectionHeadingsChunk.Length = ChunkArrayPtr[count].Length;
      }
      else
      if (CurrentChunkType == 0x0009)
      {
         FontChunk.VBN = ChunkArrayPtr[count].VBN;
         FontChunk.VBNbyte = ChunkArrayPtr[count].VBNbyte;
         FontChunk.Length = ChunkArrayPtr[count].Length;
         break;
      }
      else
      if (CurrentChunkType == 0x000d)
      {
         SymbolsChunk.VBN = ChunkArrayPtr[count].VBN;
         SymbolsChunk.VBNbyte = ChunkArrayPtr[count].VBNbyte;
         SymbolsChunk.Length = ChunkArrayPtr[count].Length;
      }
   }
}

/*****************************************************************************/
/*
A 'chunk' consist of 1..n bytes, read beginning a specified byte within a 
specified VBN of the Bookreader file.  Dynamically allocate (or reallocate) 
space for the "chunk" buffer.

The record structure is a little complex, and I haven't bothered to fathom it 
all out ... this works for HYPERREADER.  If there is only one record in the
chunk  then it is straight-forward, read the record, end of story.  If there
are two  or more records then ALL BUT THE LAST record have record-related data
(not  Bookreader information) in the LAST 10 BYTES and ALL in the FIRST 6 BYTES
of  each record.  HYPERREADER concatenates all records making up a chunk and
these 16  bytes information can be safely ignored.  Instead of expensive memory
copying  of record data to adjust these offsets on subsequent reads, the last
10 bytes  of the previous record are just written over and the first 6 bytes of
the next  record positioned to overwrite the 6 bytes prior to the last 10 of
the  previous record.  These 6 bytes are buffered and restored.
*/ 

GetChunk (struct ChunkDataStruct *ChunkDataPtr)

{
   int  status,
        RecordCount = 0,
        ChunkBufferRemaining;
   unsigned char  Buffer6 [6];

   /*********/
   /* begin */
   /*********/

   if (Debug)
      fprintf (stdout, "GetChunk() VBN: %d VBNbyte %d Length: %d\n",
               ChunkDataPtr->VBN, ChunkDataPtr->VBNbyte, ChunkDataPtr->Length);

   /* allocate enough memory to buffer the entire "chunk" of the book */
   if (Debug)
      fprintf (stdout, "PreviousChunkLength: %d ChunkDataPtr->Length: %d\n",
               PreviousChunkLength, ChunkDataPtr->Length);
   if (PreviousChunkLength < ChunkDataPtr->Length)
   {
      if (ChunkBufferPtr) free (ChunkBufferPtr);
      ChunkBufferPtr = calloc (1, PreviousChunkLength =
                                  ChunkDataPtr->Length +
                                  BookFileLongestRecordLength);
      if (!ChunkBufferPtr)
      {
         status = vaxc$errno;
         CgiLibResponseError (FI_LI, status, ErrorCalloc);

         /* definitely exit if we can't get memory!!! */
         exit (SS$_NORMAL);
      }
   }
   DynamicMemoryAllocated += ChunkDataPtr->Length;

   /* prepare for reading the record via Record File Address (RFA) */
   BookFileRab.rab$l_rfa0 = ChunkDataPtr->VBN;
   BookFileRab.rab$w_rfa4 = ChunkDataPtr->VBNbyte;
   BookFileRab.rab$b_rac = RAB$C_RFA;

   BookFileRab.rab$l_ubf = (char*)ChunkBufferPtr;
   ChunkBufferRemaining = ChunkDataPtr->Length +
                          BookFileLongestRecordLength;
   if (ChunkBufferRemaining <= BookFileLongestRecordLength)
      BookFileRab.rab$w_usz = ChunkBufferRemaining;
   else
      BookFileRab.rab$w_usz = BookFileLongestRecordLength;

   /* accumulate the total bytes read in global storage to be used later */
   CurrentChunkLength = 0;

   for (;;)
   {
      if (Debug)
         fprintf (stdout,
                  "ChunkBufferRemaining: %d BookFileRab.rab$w_usz: %d\n",
                  ChunkBufferRemaining, BookFileRab.rab$w_usz);
      if (BookFileVarRec)
         status = sys$get (&BookFileRab, 0, 0);
      else
         status = GetRecord (&BookFileRab);
      if (VMSnok (status)) break;
      if (Debug)
         fprintf (stdout, "RecordCount: %d BookFileRab.rab$w_rsz: %d\n",
                  RecordCount, BookFileRab.rab$w_rsz);
      TotalBytesRead += BookFileRab.rab$w_rsz;

      /* on record 2 ... last restore the overwritten 6 bytes */
      if (RecordCount++) memcpy (BookFileRab.rab$l_ubf, Buffer6, 6);

      /* break if we've reached the requested chunk size */
      if ((CurrentChunkLength += BookFileRab.rab$w_rsz) >=
          ChunkDataPtr->Length) break;

      /*
          Append any subsequent record to what has been read before.
          Ignore tha last 10 bytes of the record just read, and also
          overwrite the 6 bytes prior to that, buffering it first
          so that it may be restored afterwards.  As this is not the
          last record reduce the accumulated chunk length by 10 + 6.
      */
      BookFileRab.rab$l_ubf += BookFileRab.rab$w_rsz - 16;
      ChunkBufferRemaining -= BookFileRab.rab$w_rsz - 16; 
      if (ChunkBufferRemaining <= BookFileLongestRecordLength)
         BookFileRab.rab$w_usz = ChunkBufferRemaining;
      else
         BookFileRab.rab$w_usz = BookFileLongestRecordLength;
      CurrentChunkLength -= 16;
      memcpy (Buffer6, BookFileRab.rab$l_ubf, 6); 

      /* the first read is by RFA, subsequent reads are done sequentially */
      BookFileRab.rab$b_rac = RAB$C_SEQ;
   }
   if (VMSnok (status))
   {
      CgiLibResponseError (FI_LI, status, ErrorChunkRead);
      exit (status | STS$M_INHIB_MSG);
   }

   ChunkPtr = ChunkBufferPtr;
   EndChunkPtr = ChunkBufferPtr + CurrentChunkLength;
   CurrentChunkType = *(short*)ChunkBufferPtr;
   if (Debug)
      fprintf (stdout, "ChunkPtr: %d EndChunkPtr: %d CurrentChunkLength: %d\n",
               ChunkPtr, EndChunkPtr, CurrentChunkLength);

   if (DebugBytes) DumpBytes ("GETCHUNK:", ChunkBufferPtr, CurrentChunkLength);

   return (SS$_NORMAL);
}

/*****************************************************************************/
/*
For file formats that are non-VAR, as are (often?) NFS-served books which are
(always?) UDF (undefined), use this function to emulate VAR record format
record 'get's from the file.  Uses block IO and some jiggery-pokery with the
RFA and previous RSZ fields as appropriate.  Bit clunky but I guess that fits
in well with the rest of the application.
*/
 
int GetRecord (struct RAB *rabptr)

{
   int  status,
        BlockCount,
        BlockNumber;
   unsigned long  vbn;
   unsigned short  byte, usz, rsz;
   char  *ubf, *recptr;
   /* maximum buffer space needed will be 32767 plus two blocks */
   char  BlockBuffer [(512*64)+(512*2)];

   /*********/
   /* begin */
   /*********/

   if (Debug)
      fprintf (stdout, "GetRecord() rac:%d usz:%d rsz:%d rfa0:%d rfa4:%d\n",
               rabptr->rab$b_rac, rabptr->rab$w_usz, rabptr->rab$w_rsz,
               rabptr->rab$l_rfa0, rabptr->rab$w_rfa4);

   rabptr->rab$w_usz = BookFileLongestRecordLength;
   if (rabptr->rab$b_rac != RAB$C_RFA)
   {
      /* calculate VBN/byte of the next record using previous record size */
      vbn = rabptr->rab$w_rsz / 512;
      byte = rabptr->rab$w_rsz % 512;
      rabptr->rab$l_rfa0 += vbn;
      if (rabptr->rab$w_rfa4 + byte < 512)
         rabptr->rab$w_rfa4 += byte;
      else
      {
         rabptr->rab$l_rfa0++;
         rabptr->rab$w_rfa4 = byte - 512;
      }
      if (Debug)
         fprintf (stdout, "rfa0:%d rfa4:%d\n",
                  rabptr->rab$l_rfa0, rabptr->rab$w_rfa4);
   }

   BlockNumber = rabptr->rab$l_rfa0;
   BlockCount = (rabptr->rab$w_usz / 512) + 2;
   if (Debug) fprintf (stdout, "bkt:%d cnt:%d\n", BlockNumber, BlockCount);

   ubf = rabptr->rab$l_ubf; 
   usz = rabptr->rab$w_usz; 
   rabptr->rab$l_bkt = BlockNumber; 
   rabptr->rab$l_ubf = BlockBuffer; 
   rabptr->rab$w_usz = BlockCount * 512; 

   status = sys$read (&BookFileRab, 0, 0);
   if (Debug) fprintf (stdout, "sys$read() %%X%08.08X\n", status);

   rabptr->rab$l_ubf = ubf; 
   rabptr->rab$w_usz = usz; 

   if (VMSnok (status)) return (status);

   recptr = BlockBuffer + rabptr->rab$w_rfa4;
   rsz = *(unsigned short*)recptr;
   if (Debug) fprintf (stdout, "rsz:%d\n", rsz);
   if (rsz > usz) return (RMS$_UBF); 
   recptr += 2;
   memcpy (rabptr->rab$l_ubf, recptr, rsz); 
   rabptr->rab$w_rsz = rsz;

   return (status);
}

/*****************************************************************************/
/*
*/
 
int OpenBookFile ()

{
   int  status;

   /*********/
   /* begin */
   /*********/

   if (Debug) fprintf (stdout, "OpenBookFile() %s\n", BookFileNamePtr);

   /* file access block */
   BookFileFab = cc$rms_fab;
   BookFileFab.fab$b_fac = FAB$M_GET | FAB$M_BRO;
   BookFileFab.fab$l_dna = "DECW$BOOK:.DECW$BOOK";
   BookFileFab.fab$b_dns = 20;
   BookFileFab.fab$b_shr = FAB$M_SHRGET;
   BookFileFab.fab$l_xab = &BookFileXabDat;
   BookFileXabDat = cc$rms_xabdat;
   BookFileXabDat.xab$l_nxt = &BookFileXabFhc;
   BookFileXabFhc = cc$rms_xabfhc;

#ifdef ODS_EXTENDED
   if (OdsExtended)
   {
      BookFileFab.fab$l_fna = (char*)-1;
      BookFileFab.fab$b_fns = 0;
      BookFileFab.fab$l_nam = (struct namdef*)&BookFileNaml;

      ENAMEL_RMS_NAML(BookFileNaml)
      BookFileNaml.naml$l_long_filename = BookFileNamePtr;
      BookFileNaml.naml$l_long_filename_size = strlen(BookFileNamePtr);
      BookFileNaml.naml$l_long_expand = ExpBookFileName;
      BookFileNaml.naml$l_long_expand_alloc = sizeof(ExpBookFileName)-1;
   }
   else
#endif /* ODS_EXTENDED */
   {
      BookFileFab.fab$l_fna = BookFileNamePtr;
      BookFileFab.fab$b_fns = strlen(BookFileNamePtr);
      BookFileFab.fab$l_nam = &BookFileNam;

      BookFileNam = cc$rms_nam;
      BookFileNam.nam$l_esa = ExpBookFileName;
      BookFileNam.nam$b_ess = ODS2_MAX_FILE_NAME_LENGTH;
   }

   if (VMSok (status = sys$open (&BookFileFab, 0, 0)))
   {
      /* terminate 'ExpBookFileName' at the version delimiter */
#ifdef ODS_EXTENDED
      if (OdsExtended)
         *BookFileNaml.naml$l_long_ver = '\0';
      else
#endif /* ODS_EXTENDED */
         *BookFileNam.nam$l_ver = '\0';

      if (*CgiFormVarRecPtr)
      {
         if (*CgiFormVarRecPtr == '1' ||
             toupper(*CgiFormVarRecPtr) == 'Y')
            BookFileVarRec = true;
         else
            BookFileVarRec = false;
      }
      else
      if (BookFileFab.fab$b_rfm == FAB$C_VAR)
         BookFileVarRec = true;
      else
         BookFileVarRec = false;

      /* record access block */
      BookFileRab = cc$rms_rab;
      BookFileRab.rab$l_fab = &BookFileFab;
      if (BookFileVarRec)
      {
         /* 3 buffers, read ahead performance option */
         BookFileRab.rab$b_mbf = 3;
         BookFileRab.rab$l_rop = RAB$M_RAH;
      }
      else
         BookFileRab.rab$l_rop = RAB$M_BIO;

      if (CgiHttpIfModifiedSincePtr[0])
      {
         if (VMSnok (status =
             ModifiedSince (&IfModifiedSinceBinaryTime,
                            &BookFileXabDat.xab$q_rdt)))
         {
            /* book has not been modified since the date/time, don't send */
            return (status);
         }
      }

      HttpGmTimeString (LastModifiedGmDateTime, &BookFileXabDat.xab$q_rdt);

      if (Debug) fprintf (stdout, "lrl: %d\n", BookFileXabFhc.xab$w_lrl);
      if (!(BookFileLongestRecordLength = BookFileXabFhc.xab$w_lrl))
         BookFileLongestRecordLength = 32767;

      return (sys$connect (&BookFileRab, 0, 0));
   }

   if (CgiFormTitlePtr[0])
      CgiLibResponseError (FI_LI, status, CgiFormTitlePtr);
   else
   if (CgiPathInfoPtr[0])
      CgiLibResponseError (FI_LI, status, CgiPathInfoPtr);
   else
      CgiLibResponseError (FI_LI, status, CgiFormFilePtr);
   return (status);
}

/*****************************************************************************/
/*
Create a series of navigation buttons, e.g. "next", "previous", "contents", 
"tables", "figures", "index", etc.  These are provided at the top and bottom 
of each page.  The buttons can be either highlighted anchors (if the button 
represents an available link) or just "there" (if there is no relevant link, 
i.e. on the "contents" page it is redundant to have a "contents" link) as 
appropriate.
*/ 
ButtonBar (int Top1Bottom2)

{
   int  idx;
   char  *cptr, *sptr;

   /*********/
   /* begin */
   /*********/

   if (Debug) fprintf (stdout, "ButtonBar()\n");

   TBW += fprintf (stdout, "<div class=\"buttonbar%s\">\n",
                   Top1Bottom2 == 1 ? " butbartop" : "");

   cptr = ButtonsPtr;

   /* previous */
   cptr = ButtonBarButton (cptr, NULL, PreviousChunkNumber, DoMassage);

   /* back */
   cptr = ButtonBarButton (cptr, "", -1, false);

   /* next */
   cptr = ButtonBarButton (cptr, NULL, NextChunkNumber, DoMassage);

   /* divisions provided in the book itself */
   for (idx = 1; idx <= BookDivisionCount; idx++)
   {
      if (GetChunkNumber == BookDivision[idx].ChunkNumber)
         ButtonBarButton (BookDivision[idx].DescriptionPtr, "", 0, DoMassage);
      else
         ButtonBarButton (BookDivision[idx].DescriptionPtr, "",
                          BookDivision[idx].ChunkNumber, DoMassage);
   }

   /* close */
   if (CgiFormRefererPtr[0])
      cptr = ButtonBarButton (cptr, CgiFormRefererPtr, -1, false);
   else
      cptr = ButtonBarButton (cptr, NULL, -1, false);

   /* further buttons (starting with "help") */
   while (*cptr) cptr = ButtonBarButton (cptr, NULL, -1, false);

   TBW += fprintf (stdout, "</div>\n");
}

/*****************************************************************************/
/*
Generate a single "button" inside the context created by ButtonBar().
*/

char* ButtonBarButton
(
char *ButtonLabel,
char *ButtonPath,
int ChunkNumber,
BOOL NavMassage
)
{
   char  *cptr, *mptr, *sptr, *tptr, *uptr;

   /*********/
   /* begin */
   /*********/

   if (Debug) fprintf (stdout, "ButtonBarButton() |%s|\n", ButtonLabel);

   if (NavMassage)
      mptr = "";
   else
      mptr = "&massage=no";

   if (!*ButtonLabel)
      for (cptr = ButtonLabel = "*ERROR*"; *cptr; cptr++);
   else
      for (cptr = ButtonLabel; *cptr && *cptr != '=' && *cptr != '$'; cptr++);

   if (*ButtonLabel == '^')
   {
      ButtonLabel++;
      tptr = " target=\"_blank\"";
   }
   else
      tptr = "";

   if (ChunkNumber == 0)
   {
      /* no chunk number therefore no link, just a label */
      TBW += fprintf (stdout, "<a class=\"nolink\">%*.*s</a>\n",
                      cptr-ButtonLabel, cptr-ButtonLabel, ButtonLabel);
   }
   else
   if (ChunkNumber > 0)
   {
      /* link to this particular chunk of the book */
      TBW += fprintf (stdout,
"<a href=\"%s%s?%schunk=%d&referer=%s&title=%s%s\">%*.*s</a>\n",
                      CgiScriptNamePtr, UrlEncodedCgiPathInfoPtr,
                      BookSpecifiedByFile, ChunkNumber, UrlEncReferer,
                      UrlEncTitle, mptr,
                      cptr-ButtonLabel, cptr-ButtonLabel, ButtonLabel);
   }
   else
   {
      /* -1 == SPECIAL CASE, create non-chunk button */
      if (*cptr == '=')
         for (sptr = uptr = cptr+1; *sptr && *sptr != '$'; sptr++);
      else
      if (ButtonPath)
         sptr = (uptr = ButtonPath) + strlen(ButtonPath);
      else
         sptr = (uptr = "*ERROR*") + 8;

      if (!memcmp(uptr,"javascript:", 11))
         TBW += fprintf (stdout,
"<script type=\"text/javascript\">\
document.write(\'<a%s href=\"%*.*s\">%*.*s</a>\\n\');\
</script>\n",
                         tptr, sptr-uptr, sptr-uptr, uptr,
                         cptr-ButtonLabel, cptr-ButtonLabel, ButtonLabel);
      else
         TBW += fprintf (stdout, "<a%s href=\"%*.*s\">%*.*s</a>\n",
                         tptr, sptr-uptr, sptr-uptr, uptr,
                         cptr-ButtonLabel, cptr-ButtonLabel, ButtonLabel);
   }

   while (*cptr && *cptr != '$') cptr++;
   while (*cptr && *cptr == '$') cptr++;

   return (cptr);
}

/*****************************************************************************/
/*
Return a bitmap image to the client as a GIF image.

The code in these functions is implemented in accordance with Compuserve's 
Graphic Interchange Format Programming Reference specification, version 89a, 
31st July 1990. 

The LZW compression employed by the GIF algorithm is implemented using code 
derived from the PBM suite.  Two functions, virtually unmodified, are 
employed, GifCompress() ... formally called 'compgif()', and GifPackBits() ... 
formally called 'pack_bits()'.  The original commentary and copyright notice 
remains as per the code author's request.
*/ 

GifImage
(
int WidthPixels, 
int HeightPixels, 
unsigned char *ImageBytePtr,
int  ByteCount
)
{
   static int  Background = 0,
               BitsPerPixel = 1;
   /* background (index 0) is white, foreground (index 1) is black */
   static unsigned char  Red [] = { 0xff, 0x00 },
                         Green [] = { 0xff, 0x00 },
                         Blue [] = { 0xff, 0x00 };

   int  idx,
        Byte,
        LeftOffset,
        TopOffset,
        Resolution,
        ColorMapSize,
        InitCodeSize;
   char  *bptr;
   char  Buffer [512];

   /*********/
   /* begin */
   /*********/

   if (Debug) fprintf (stdout, "GifImage()\n");

   CgiLibResponseHeader (200, "image/gif",
                         "Last-Modified: %s\n", LastModifiedGmDateTime);

   /* initialize the following functions */
   GifNextPixel (ImageBytePtr, ByteCount);
   GifPackBits (-1, -1);
   
   ColorMapSize = 1 << BitsPerPixel;
   LeftOffset = TopOffset = 0;
   Resolution = BitsPerPixel;

   /* 
      BookReader Format (BRF) images are always whole bytes.
      Image width specifications (in bits) do not always appear to be so!
      If the width is not an even number of bits (making up a whole byte)
      then make it so by rounding up to the next whole byte number of bits.
      Seems to work OK!
   */
   if (WidthPixels & 0x7) WidthPixels = ((WidthPixels >> 3) + 1) << 3;

   /* the initial code size */
   if( BitsPerPixel <= 1 )
      InitCodeSize = 2;
   else
      InitCodeSize = BitsPerPixel;

   /**************************/
   /* GIF Data Stream header */
   /**************************/

   /* accumulate bytes into buffer before output */
   bptr = Buffer;

   strcpy (bptr, "GIF89a");
   bptr += 6;

   /*****************************/
   /* Logical Screen Descriptor */
   /*****************************/

   /* width and height of logical screen */
   *bptr++ = WidthPixels & 0xff;
   *bptr++ = (WidthPixels >> 8) & 0xff;
   *bptr++ = HeightPixels & 0xff;
   *bptr++ = (HeightPixels >> 8) & 0xff;

   /* indicate that there is a global colour map */
   Byte = 0x80;
   /* OR in the resolution */
   Byte |= (Resolution - 1) << 5;
   /* OR in the Bits per Pixel */
   Byte |= (BitsPerPixel - 1);
   /* write it out */
   *bptr++ = Byte;

   /* Background colour */
   *bptr++ = Background;

   /* pixel aspect ratio */
   *bptr++ = 0;

   /***********************/
   /* Global Colour Table */
   /***********************/

   for (idx = 0; idx < ColorMapSize; idx++)
   {
      *bptr++ = Red[idx];
      *bptr++ = Green[idx];
      *bptr++ = Blue[idx];
   }

   /****************************************/
   /* Graphic Control Extension descriptor */
   /****************************************/

   /* extension introducer and graphic control label */
   *bptr++ = 0x21;
   *bptr++ = 0xf9;
   /* fixed size of the following data block */
   *bptr++ = 0x04;
   /* Transparency Index is provided */
   *bptr++ = 0x01;
   /* no data in these */
   *bptr++ = 0x00;
   *bptr++ = 0x00;
   /* Transparent Color Index value, BACKGROUND SHOULD BE TRANSPARENT */
   *bptr++ = 0x00;
   /* block terminator */
   *bptr++ = 0x00;

   /********************/
   /* Image descriptor */
   /********************/

   /* write an Image separator */
   *bptr++ = 0x2c;

   /* location of image within logical screen */
   *bptr++ = LeftOffset & 0xff;
   *bptr++ = (LeftOffset >> 8) & 0xff;
   *bptr++ = TopOffset & 0xff;
   *bptr++ = (TopOffset >> 8) & 0xff;

   /* width and height of image within logical screen */
   *bptr++ = WidthPixels & 0xff;
   *bptr++ = (WidthPixels >> 8) & 0xff;
   *bptr++ = HeightPixels & 0xff;
   *bptr++ = (HeightPixels >> 8) & 0xff;

   /* no local color table, image is not interlaced, not ordered, etc. */
   *bptr++ = 0x00;

   /**************************/
   /* table-based image data */
   /**************************/

   /* write out the initial code size */
   *bptr++ = InitCodeSize;

   /* transfer what we've accumlated in the local buffer */
   fwrite (Buffer, bptr-Buffer, 1, stdout);

   /* LZW compress the data using PBM-derived algorithm and code */
   GifCompress (InitCodeSize + 1);

   /****************************************/
   /* end of image data and GIF terminator */
   /****************************************/

   /* write out a zero-length packet (to end the series), and terminator */
   fwrite ("\0;", 2, 1, stdout);
}

/*****************************************************************************/
/*
Get a series of pixels from the bitmap.  This function is called by 
GifCompress() to scan the image.  Bit 0 through to bit 7 of the current byte 
are returned as 1 or 0 before the next byte is pointed to.  When the total 
number of bytes have been transmitted return EOF.  This function is 
initialized by a first call, setting the values of the byte pointer and number 
of bytes in the image.
*/ 

GifNextPixel
(
unsigned char *ImageBytePtr,
int ImageByteCount
)
{
   static unsigned int  BitPosition,
                        ByteCount;
   static unsigned char  *BytePtr;

   /*********/
   /* begin */
   /*********/

   if (ImageBytePtr)
   {
      /* initialize */
      BytePtr = ImageBytePtr;
      ByteCount = ImageByteCount;
      BitPosition = 1;
      return;
   }

   if (!BitPosition)
   {
      /* bits 0 through to 7 have been returned, move to next byte */
      if (!ByteCount) return (EOF);
      ByteCount--;
      BytePtr++;
      BitPosition = 1;
   }

   if (*BytePtr & BitPosition)
   {
      /* next call will return the next most significant bit (if <= 7) */
      BitPosition = (BitPosition << 1) & 0xff;
      /* the tested bit position contained a 1 (foreground) */
      return (1);
   }
   else
   {
      /* next call will return the next most significant bit (if <= 7) */
      BitPosition = (BitPosition << 1) & 0xff;
      /* the tested bit position contained a 0 (background) */
      return (0);
   }
}

/****************************************************************************/
/*
 * This software is copyrighted as noted below.  It may be freely copied,
 * modified, and redistributed, provided that the copyright notice is 
 * preserved on all copies.
 * 
 * There is no warranty or other guarantee of fitness for this software,
 * it is provided solely "as is".  Bug reports or fixes may be sent
 * to the author, who may or may not act on them as he desires.
 *
 * You may not include this software in a program or other software product
 * without supplying the source, or without informing the end-user that the 
 * source is available for no extra charge.
 *
 * If you modify this software, you should include a notice giving the
 * name of the person performing the modification, the date of modification,
 * and the reason for such modification.
 */

/*  compgif.c */
/*
 *
 * GIF Image compression - LZW algorithm implemented with Trie type
 *                         structure.
 *                         Written by Bailey Brown, Jr.
 *                         last change May 24, 1990
 *                         file: compgif.c
 *
 *  You may use or modify this code as you wish, as long as you mention
 *  my name in your documentation.
 *
 *                  - Bailey Brown, Jr.
 *
 */

#define MAXIMUMCODE 4095   /* 2**maximum_code_size */
#define BLOCKSIZE 256   /* max block byte count + 1 */
#define NULLPREFIX -1

typedef struct str_table_entry {
        int code;
        int prefix;
        int suffix;
}  strTableEntry;

typedef struct str_table_node {
        strTableEntry entry;
    struct str_table_node *left;
    struct str_table_node *right;
struct str_table_node *children;
} strTableNode, *strTableNodePtr, **strTable;

/*
 ********************************************************************
 * compgif() recieves pointers to an input function and an output    *
 * stream, and the code size as parameters and outputs successive   *
 * blocks of LZW compressed gif data.  The calling routine should   *
 * have aready written the GIF file header out to the output file.  *
 * It assumes that there will be no more than 8 bits/pixel and that *
 * each data item comes from successive bytes returned by infun.    *
 ********************************************************************
 */

int GifCompress (int code_size)

{
    strTable heap; /* our very own memory manager */
    int heap_index;
    int clear_code, end_code, cur_code;
    int i, found, num_colors, prefix, compress_size;
    int cur_char, end_of_data, bits_per_pix;
    strTableNodePtr cur_node;
    strTable root;  /* root of string table for LZW compression is */
                    /* an array of 2**bits_per_pix pointers to atomic nodes */
    heap_index = 0;
    heap = (strTable)malloc(sizeof(strTableNodePtr)*MAXIMUMCODE);
    if (!heap) printf("can't allocate heap");
    for (i=0; i < MAXIMUMCODE; i++) {
        heap[i] = (strTableNodePtr)malloc(sizeof(strTableNode));
        if (!heap[i])
        {
/*
           printf("can't allocate heap");
*/
           int  status;

           status = vaxc$errno;
           CgiLibResponseError (FI_LI, status, ErrorCalloc);

           /* definitely exit if we can't get memory!!! */
           exit (SS$_NORMAL);
        }
    }
    bits_per_pix = code_size - 1;
    compress_size = code_size;
    num_colors = 1<<(bits_per_pix);
    clear_code = num_colors;
    end_code = clear_code + 1;
    cur_code = end_code + 1;
    prefix = NULLPREFIX;
    root = (strTable)malloc(sizeof(strTableNodePtr)*num_colors);
    if (!root)
    {
/*
       printf("memory allocation failure (root)");
*/
       int  status;

       status = vaxc$errno;
       CgiLibResponseError (FI_LI, status, "malloc()");
       /* definitely exit if we can't get memory!!! */
       exit (SS$_NORMAL);
    }
    for(i=0; i<num_colors; i++) {
        root[i] = heap[heap_index++];
        root[i]->entry.code = i;
        root[i]->entry.prefix = NULLPREFIX;
        root[i]->entry.suffix = i;
        root[i]->left = NULL;
        root[i]->right = NULL;
        root[i]->children = NULL;
    }
    /* initialize  output block */
    GifPackBits(compress_size, -1);
    GifPackBits(compress_size, clear_code);
    end_of_data = 0;
    if ((cur_char = GifNextPixel(NULL, 0)) == EOF)
       printf("premature end of data");
    while (!end_of_data) {
        prefix = cur_char;
        cur_node = root[prefix];
        found = 1;
        if((cur_char = GifNextPixel(NULL, 0)) == EOF) {
            end_of_data = 1; break;
        }
        while(cur_node->children && found) {
            cur_node = cur_node->children;
            while(cur_node->entry.suffix != cur_char) {
                if (cur_char < cur_node->entry.suffix) {
                    if (cur_node->left) cur_node = cur_node->left;
                    else {
                        cur_node->left = heap[heap_index++];
                        cur_node = cur_node->left;
                        found = 0; break;
                    }
                }
                else {
                    if (cur_node->right) cur_node = cur_node->right;
                    else {
                        cur_node->right = heap[heap_index++];
                        cur_node = cur_node->right;
                        found = 0; break;
                    }
                }
            }
            if (found) {
                prefix = cur_node->entry.code;
                if((cur_char = GifNextPixel(NULL, 0)) == EOF) {
                    end_of_data = 1; break;
                }
            }
        }
        if (end_of_data) break;
        if (found) {
            cur_node->children = heap[heap_index++];
            cur_node = cur_node->children;
        }
        cur_node->children = NULL;
        cur_node->left = NULL;
        cur_node->right = NULL;
        cur_node->entry.code = cur_code;
        cur_node->entry.prefix = prefix;
        cur_node->entry.suffix = cur_char;
        GifPackBits(compress_size, prefix);
        if (cur_code > ((1<<(compress_size))-1))
            compress_size++;
        if (cur_code < MAXIMUMCODE) {
            cur_code++;
        }
        else {
            heap_index = num_colors;  /* reinitialize string table */
            for (i=0; i < num_colors; i++ ) root[i]->children = NULL;
            GifPackBits(compress_size, clear_code);
            compress_size = bits_per_pix + 1;
            cur_code = end_code + 1;
        }
    }
    GifPackBits(compress_size, prefix);
    GifPackBits(compress_size, end_code);
    GifPackBits(compress_size, -1);
    for (i=0; i < MAXIMUMCODE; i++) free(heap[i]);
    free(heap);
    free(root);
    return (1);
}

/*
 ************************************************************************
 * GifPackBits() packs the bits of the codes generated by gifenc() into   *
 * a 1..256 byte output block.  The first byte of the block is the      *
 * number 0..255 of data bytes in the block.  To flush or initialize    *
 * the block, pass a negative argument.                                 *
 ************************************************************************
 */

GifPackBits (int compress_size, int prefix)

{
    static int cur_bit = 8;
    static unsigned char block[BLOCKSIZE] = { 0 };
    int i, left_over_bits;

    if (compress_size == -1 && prefix == -1)
    {
       /* initialize */
       cur_bit = 8;
       block[0] = 0;
       return;
    }

    /* if we are about to excede the bounds of block or if the flush
       code (code_bis < 0) we output the block */
    if((cur_bit + compress_size > (BLOCKSIZE-1)*8) || (prefix < 0)) {
        /* handle case of data overlapping blocks */
        if ((left_over_bits = (((cur_bit>>3) +
                ((cur_bit & 7) != 0))<<3) - cur_bit) != 0) {
            for (i=0; i < left_over_bits; i++) {
                if (prefix & (1<<i))
                   block[cur_bit>>3] |= (char)(1<<(cur_bit & 7));
                /* note n>>3 == n/8 and n & 7 == n % 8 */
                cur_bit++;
            }
        }
        compress_size -= left_over_bits;
        prefix = prefix>>left_over_bits;
        block[0] =  (unsigned char)((cur_bit>>3) - 1);

        if (block[0]) fwrite (block, block[0]+1, 1, stdout);

        for(i=0; i < BLOCKSIZE; i++) block[i] = 0;
        cur_bit = 8;
    }
    if (prefix >= 0) {
        for (i=0; i < compress_size; i++) {
           if (prefix & (1<<i))
           block[cur_bit>>3] |= (unsigned char)(1<<(cur_bit & 7));
           /* note n>>3 == n/8 and n & 7 == n % 8 */
           cur_bit++;
        }
    }
    return (1);
}

/*****************************************************************************/
/*
*/
 
InsertComment
(
int Horizontal, 
int Vertical, 
char *Explanation,
int SourceCodeLineNumber
)
{
   int  Length,
        Line,
        Column;
   char  String [256];
   struct OutCharStruct  *ocptr;

   /*********/
   /* begin */
   /*********/

   if (Debug) fprintf (stdout, "InsertComment()\n");

   Line = Vertical / LINE_POSITIONING_UNITS;
   if (!Line) Line = 1;
   if (Line > LinesOnPage) PageSize (Line);
   if (Line > LastLineOnPage) LastLineOnPage = Line;
   Column = Horizontal / COLUMN_POSITIONING_UNITS;
   if (Column >= MAX_OUTLINE_LENGTH) Column = MAX_OUTLINE_LENGTH-1;
   if (Debug) fprintf (stdout, "Line: %d Column: %d\n", Line, Column);

   Length = sprintf (String, "%s <!-- source code line number: %d -->",
                     Explanation, SourceCodeLineNumber);

   ocptr = OUT_CHAR_AT(Line,Column);
   ocptr->htptr = HtmlString (ocptr->htptr, Length);
   strcat (ocptr->htptr, String);
}

/*****************************************************************************/
/*
For debugging purposes.
*/
 
DumpBytes
(
char *Comment,
char *BytePtr,
int NumberOfBytes
) 
{
   static char  HexDigits [] = "0123456789abcdef";

   int  bcnt, ccnt;
   char  *bptr, *sptr;
   char  String [256];

   /*********/
   /* begin */
   /*********/

   fprintf (stdout, "\n%s Byte DUMP beginning: %08.08x  Bytes: %d\n",
            Comment, BytePtr, NumberOfBytes);
   while (NumberOfBytes > 0)
   {
      sptr = String;
      bptr = BytePtr;
      sprintf (sptr, "\n%08.08x : ", bptr);
      sptr += 12;
      ccnt = 16;
      bcnt = NumberOfBytes;
      while (ccnt-- && bcnt--)
      {
         *sptr++ = HexDigits[*bptr >> 4];
         *sptr++ = HexDigits[*bptr++ & 0x0f];
         *sptr++ = ' ';
      }
      bptr = BytePtr;
      strcpy (sptr, "\n           ");
      sptr += 12;
      ccnt = 16;
      bcnt = NumberOfBytes;
      while (ccnt-- && bcnt--)
      {
         if (isprint(*bptr))
         {
            *sptr++ = ' ';
            *sptr++ = *bptr++;
            *sptr++ = ' ';
         }
         else
         {
            *sptr++ = ' ';
            *sptr++ = '^';
            *sptr++ = ' ';
            bptr++;
         }
      }
      *sptr++ = '\n';
      *sptr = '\0';
      fputs (String, stdout);
      NumberOfBytes -= 16;
      BytePtr += 16;
   }
   fputs ("\n", stdout);
}

/*****************************************************************************/
/*
For debugging purposes.
*/
 
DumpLongWords
(
char *Comment,
unsigned long *lwptr,
int NumberOfLongWords
) 
{
   int  count = 1;

   /*********/
   /* begin */
   /*********/

   fprintf (stdout, "\n%s Longword DUMP beginning: %08.08x, Longwords: %d\n",
            Comment, lwptr, NumberOfLongWords);
   while (NumberOfLongWords-- > 0)
   {
      fprintf (stdout,
      "%2d.  %08.08x : 0x%08.08x : %10d : %5d %5d : %3d %3d %3d %3d\n",
      count++, lwptr, *lwptr, *lwptr,
      *lwptr >> 16, *lwptr & 0xffff,
      (*lwptr & 0xff000000) >> 24,
      (*lwptr & 0xff0000) >> 16,
      (*lwptr & 0xff00) >> 8,
      *lwptr & 0xff);
      lwptr++;
   }
   fputs ("\n", stdout);
}

/*****************************************************************************/
/*
If the object has been modified since the specified date and time then return 
a normal status indicating that the data transfer is to continue.  If not 
modified then send a "not modified" HTTP header and return an error status to 
indicate the object should not be sent.
*/ 
 
int ModifiedSince
(
unsigned long *SinceBinaryTimePtr,
unsigned long *BinaryTimePtr
)
{
   static unsigned long  OneSecondDelta [2] = { -10000000, -1 };

   int  status;
   unsigned long  AdjustedBinTime [2],
                  ScratchBinTime [2];

   /*********/
   /* begin */
   /*********/

   if (Debug) fprintf (stdout, "ModifiedSince()\n");

   /* if request asks for a "reload" (not cached) then give it regardless */
   if (strsame (CgiHttpPragmaPtr, "no-cache", -1)) return (SS$_NORMAL);
   if (strsame (CgiHttpCacheControlPtr, "no-cache", 8)) return (SS$_NORMAL);
   if (strsame (CgiHttpCacheControlPtr, "no-store", 8)) return (SS$_NORMAL);
   if (strsame (CgiHttpCacheControlPtr, "max-age=0", 9)) return (SS$_NORMAL);

   /*
      Add one second to the modified time.  Ensures a negative time
      for VMS where fractional seconds may result in inconclusive
      results when the target time is being specified in whole seconds.
   */ 
   if (VMSnok (status =
       lib$add_times (SinceBinaryTimePtr, &OneSecondDelta, &AdjustedBinTime)))
   {
      CgiLibResponseError (FI_LI, status, "lib$add_times()");
      return (status);
   }
   if (Debug) fprintf (stdout, "sys$add_times() %%X%08.08X\n", status);

   /* if a positive time results the file has been modified */
   if (VMSok (status =
       lib$sub_times (BinaryTimePtr, &AdjustedBinTime, &ScratchBinTime)))
      return (status);

   if (Debug) fprintf (stdout, "sys$sub_times() %%X%08.08X\n", status);
   if (status != LIB$_NEGTIM)
   {
      CgiLibResponseError (FI_LI, status, "sys$sub_times()");
      return (status);
   }

   CgiLibResponseHeader (304, "text/html");
   fprintf (stdout, "Not modified!\n");

   return (LIB$_NEGTIM);
}

/*****************************************************************************/
/*
Create an HTTP format Greenwich Mean Time (UTC) time string in the storage 
pointed at by 'TimeString', e.g. "Fri, 25 Aug 1995 17:32:40 GMT" (RFC 1123). 
This must be at least 30 characters capacity.  If 'BinTimePtr' is null the 
time string represents the current time.  If it points to a quadword, VMS time 
value the string represents that time.  'TimeString' must point to storage 
large enough for 31 characters.
*/

int HttpGmTimeString
(
char *TimeString,
unsigned long *BinTimePtr
)
{
   static char  *DayNames [] =
      { "", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun" };
   static char  *MonthName [] =
      { "", "Jan", "Feb", "Mar", "Apr", "May", "Jun",
            "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" };

   static $DESCRIPTOR (HttpTimeFaoDsc, "!AZ, !2ZW !AZ !4ZW !2ZW:!2ZW:!2ZW GMT");
   static $DESCRIPTOR (TimeStringDsc, "");

   int  status;
   unsigned long  BinTime [2],
                  GmTime [2];
   unsigned short  Length;
   unsigned short  NumTime [7];
   unsigned long  DayOfWeek;

   /*********/
   /* begin */
   /*********/

   if (Debug) fprintf (stdout, "HttpGmTimeString()\n");

   if (!BinTimePtr)
      sys$gettim (&GmTime);
   else
   {
      GmTime[0] = BinTimePtr[0];
      GmTime[1] = BinTimePtr[1];
   }
   if (VMSnok (status = TimeAdjustGMT (true, &GmTime)))
      return (status);

   status = sys$numtim (&NumTime, &GmTime);
   if (Debug)
      fprintf (stdout, "sys$numtim() %%X%08.08X %d %d %d %d %d %d %d\n",
               status, NumTime[0], NumTime[1], NumTime[2],
               NumTime[3], NumTime[4], NumTime[5], NumTime[6]);

   if (VMSnok (status = lib$day_of_week (&GmTime, &DayOfWeek)))
      return (status);
   if (Debug)
      fprintf (stdout, "lib$day_of_week() %%X%08.08X is %d\n",
               status, DayOfWeek);

   /* set the descriptor address and size of the resultant time string */
   TimeStringDsc.dsc$w_length = 30;
   TimeStringDsc.dsc$a_pointer = TimeString;

   if (VMSnok (status =
       sys$fao (&HttpTimeFaoDsc, &Length, &TimeStringDsc,
                DayNames[DayOfWeek], NumTime[2], MonthName[NumTime[1]],
                NumTime[0], NumTime[3], NumTime[4], NumTime[5])))
   {
      if (Debug) fprintf (stdout, "sys$fao() %%X%08.08X\n", status);
      TimeString[0] = '\0';
   }
   else
      TimeString[Length] = '\0';
   if (Debug) fprintf (stdout, "|%s|\n", TimeString);

   return (status);
}

/*****************************************************************************/
/*
Given a string such as "Fri, 25 Aug 1995 17:32:40 GMT" (RFC 1123), or "Friday,
25-Aug-1995 17:32:40 GMT" (RFC 1036), create an internal, local, binary time
with the current GMT offset.  See complementary function HttpGmTimeString().
*/ 

int HttpGmTime
(
char *TimeString,
unsigned long *BinTimePtr
)
{
   static char  *MonthName [] =
      { "", "Jan", "Feb", "Mar", "Apr", "May", "Jun",
            "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" };

   int  status;
   unsigned short  Length;
   unsigned short  NumTime [7] = { 0,0,0,0,0,0,0 };
   unsigned long  DayOfWeek;
   char  *tptr;

   /*********/
   /* begin */
   /*********/

   if (Debug) fprintf (stdout, "HttpGmTime() |%s|\n", TimeString);

   tptr = TimeString;
   /* hunt straight for the comma after the weekday name! */
   while (*tptr && *tptr != ',') tptr++;
   if (*tptr) tptr++;
   /* span white space between weekday name and date */
   while (*tptr && isspace(*tptr)) tptr++;
   if (Debug) fprintf (stdout, "tptr |%s|\n", tptr);
   if (!*tptr) return (STS$K_ERROR);

   /* get the date and then skip to month name */
   if (isdigit(*tptr)) NumTime[2] = atoi (tptr);
   while (*tptr && isdigit(*tptr)) tptr++;
   while (*tptr && (*tptr == '-' || isspace(*tptr))) tptr++;
   if (Debug) fprintf (stdout, "tptr |%s|\n", tptr);
   if (!*tptr) return (STS$K_ERROR);

   /* get the month number from the name and skip to the year */
   for (NumTime[1] = 1; NumTime[1] <= 12; NumTime[1]++)
      if (strsame (tptr, MonthName[NumTime[1]], 3)) break;
   if (NumTime[1] > 12) return (STS$K_ERROR);
   while (*tptr && isalpha(*tptr)) tptr++;
   while (*tptr && (*tptr == '-' || isspace(*tptr))) tptr++;
   if (Debug) fprintf (stdout, "tptr |%s|\n", tptr);
   if (!*tptr) return (STS$K_ERROR);

   /* get the year and then skip to the hour */
   if (isdigit(*tptr))
   {
      NumTime[0] = atoi (tptr);
      if (NumTime[0] < 100) NumTime[0] += 1900;
   }
   while (*tptr && isdigit(*tptr)) tptr++;
   while (*tptr && isspace(*tptr)) tptr++;
   if (Debug) fprintf (stdout, "tptr |%s|\n", tptr);
   if (!*tptr) return (STS$K_ERROR);

   /* get the hour, minute and second */
   if (isdigit(*tptr)) NumTime[3] = atoi (tptr);
   while (*tptr && isdigit(*tptr)) tptr++;
   if (*tptr == ':') tptr++;
   if (isdigit(*tptr)) NumTime[4] = atoi (tptr);
   while (*tptr && isdigit(*tptr)) tptr++;
   if (*tptr == ':') tptr++;
   if (isdigit(*tptr)) NumTime[5] = atoi (tptr);
   while (*tptr && isdigit(*tptr)) tptr++;
   if (*tptr == ':') tptr++;
   if (!*tptr) return (STS$K_ERROR);

   /* the only thing remaining should be the "GMT" */
   while (*tptr && isspace(*tptr)) tptr++;
   if (Debug) fprintf (stdout, "tptr |%s|\n", tptr);
   if (!strsame (tptr, "GMT", 3)) return (STS$K_ERROR);

   /*******************************************/
   /* convert what looks like legitimate GMT! */
   /*******************************************/

   if (Debug)
      fprintf (stdout, "NumTime[] %d %d %d %d %d %d %d\n",
               NumTime[0], NumTime[1], NumTime[2], NumTime[3], 
               NumTime[4], NumTime[5], NumTime[6]); 
   status = lib$cvt_vectim (&NumTime, BinTimePtr);
   if (VMSnok (status)) return (status);
   if (Debug) fprintf (stdout, "lib$cvt_vectim() %%X%08.08X\n", status);

   return (TimeAdjustGMT (false, BinTimePtr));
}

/*****************************************************************************/
/*
Determine the offset from GMT (UTC) using either the HTTPD$GMT or
SYS$TIMEZONE_DIFFERENTIAL logicals. If HTTPD$GMT is not defined time should be
set from the timezone differential logical. Function RequestBegin() calls
this every hour to recheck GMT offset and detect daylight saving or other
timezone changes.
*/

int TimeSetGmt ()

{
   static BOOL  UseTimezoneDifferential = false;

   int  status;

   /*********/
   /* begin */
   /*********/

   if (Debug) fprintf (stdout, "TimeSetGmt()\n");

   if (!UseTimezoneDifferential)
   {
      status = TimeSetHttpdGmt();
      /* return if error and that error was not that the name did not exist */
      if (VMSok (status) || status != SS$_NOLOGNAM) return (status);
   }

   UseTimezoneDifferential = true;
   return (TimeSetTimezone());
}

/*****************************************************************************/
/*
The SYS$TIMEZONE_DIFFERENTIAL logical contains the number of seconds offset
from GMT (UTC) as a positive (ahead) or negative (behind) number.  Set the
'TimeGmtString' global storage to a "+hh:mm" or "-hh:mm" string equivalent, and
the 'TimeAheadOfGmt' global boolean and 'TimeGmtVmsString' delta-time global
string.
*/

int TimeSetTimezone ()

{
   static unsigned short  Length;
   static $DESCRIPTOR (TimezoneLogicalNameDsc, "SYS$TIMEZONE_DIFFERENTIAL");
   static $DESCRIPTOR (LnmSystemDsc, "LNM$SYSTEM");
   static $DESCRIPTOR (TimeGmtStringFaoDsc, "!AZ!2ZL:!2ZL");
   static $DESCRIPTOR (TimeGmtVmsStringFaoDsc, "0 !2ZL:!2ZL");
   static $DESCRIPTOR (TimeGmtStringDsc, TimeGmtString);
   static struct {
      short int  buf_len;
      short int  item;
      void  *buf_addr;
      unsigned short  *ret_len;
   } LnmItems [] =
   {
      { sizeof(TimeGmtString)-1, LNM$_STRING, TimeGmtString, &Length },
      { 0,0,0,0 }
   };

   int  status;
   long  Hours,
         Minutes,
         Seconds;
   char  *SignPtr;
   $DESCRIPTOR (TimeGmtVmsStringDsc, TimeGmtVmsString);

   /*********/
   /* begin */
   /*********/

   if (Debug) fprintf (stdout, "TimeSetTimezone()\n");

   status = sys$trnlnm (0, &LnmSystemDsc, &TimezoneLogicalNameDsc, 0, &LnmItems);
   if (Debug) fprintf (stdout, "sys$trnlnm() %%X%08.08X\n", status);
   if (VMSnok (status)) return (status);

   TimeGmtString[Length] = '\0';
   if (Debug) fprintf (stdout, "TimeGmtString |%s|\n", TimeGmtString);
   Seconds = atol(TimeGmtString);
   if (Seconds < 0)
   {
      TimeAheadOfGmt = false;
      Seconds = -Seconds;
      SignPtr = "-";
   }
   else
   {
      TimeAheadOfGmt = true;
      SignPtr = "+";
   }
   Hours = Seconds / 3600;
   Minutes = (Seconds - Hours * 3600) / 60;
   if (Debug)
      fprintf (stdout, "%d %s%d:%d\n", Seconds, SignPtr, Hours, Minutes);
   sys$fao (&TimeGmtStringFaoDsc, &Length, &TimeGmtStringDsc,
            SignPtr, Hours, Minutes);
   TimeGmtString[Length] = '\0';
   if (Debug) fprintf (stdout, "TimeGmtString |%s|\n", TimeGmtString);

   sys$fao (&TimeGmtVmsStringFaoDsc, &Length, &TimeGmtVmsStringDsc,
            Hours, Minutes);
   TimeGmtVmsString[Length] = '\0';
   if (Debug) fprintf (stdout, "TimeGmtVmsString |%s|\n", TimeGmtVmsString);

   TimeGmtVmsStringDsc.dsc$w_length = Length;

   if (VMSnok (status =
       sys$bintim (&TimeGmtVmsStringDsc, &TimeGmtDeltaBinary)))
      return (status);
   if (TimeGmtDeltaBinary[0] || TimeGmtDeltaBinary[1])
      return (status);
   /* time must have been zero, make it one, one-hundreth of a second */
   TimeGmtDeltaBinary[0] = -100000;
   TimeGmtDeltaBinary[1] = -1;
   return (SS$_NORMAL);
}

/*****************************************************************************/
/*
Translate the logical HTTPD$GMT (defined to be something like "+10:30" or "-
01:15") and convert it into a delta time structure and store in 
'TimeGmtDeltaBinary'.  Store whether it is in advance or behind GMT in boolean 
'TimeAheadOfGmt'.  Store the logical string in 'TimeGmtString'.
*/

int TimeSetHttpdGmt ()

{
   static unsigned short  Length;
   static $DESCRIPTOR (GmtLogicalNameDsc, "HTTPD$GMT");
   static $DESCRIPTOR (LnmFileDevDsc, "LNM$FILE_DEV");
   static struct {
      short int  buf_len;
      short int  item;
      void  *buf_addr;
      unsigned short  *ret_len;
   } LnmItems [] =
   {
      { sizeof(TimeGmtString)-1, LNM$_STRING, TimeGmtString, &Length },
      { 0,0,0,0 }
   };

   int  status;
   char  *cptr, *sptr;
   $DESCRIPTOR (TimeGmtVmsStringDsc, TimeGmtVmsString);

   /*********/
   /* begin */
   /*********/

   if (Debug) fprintf (stdout, "TimeSetHttpdGmt()\n");

   status = sys$trnlnm (0, &LnmFileDevDsc, &GmtLogicalNameDsc, 0, &LnmItems);
   if (Debug) fprintf (stdout, "sys$trnlnm() %%X%08.08X\n", status);
   if (VMSnok (status)) return (status);

   TimeGmtString[Length] = '\0';
   if (Debug) fprintf (stdout, "TimeGmtString |%s|\n", TimeGmtString);

   if (TimeGmtString[0] == '$') return (SS$_NORMAL);

   if (*(cptr = TimeGmtString) == '-')
      TimeAheadOfGmt = false;
   else
      TimeAheadOfGmt = true;
   if (*cptr == '+' || *cptr == '-') cptr++;
   sptr = TimeGmtVmsString;
   *sptr++ = '0';
   *sptr++ = ' ';
   while (*cptr) *sptr++ = *cptr++;
   *sptr = '\0';
   if (Debug) fprintf (stdout, "TimeGmtVmsString |%s|\n", TimeGmtVmsString);

   TimeGmtVmsStringDsc.dsc$w_length = sptr - TimeGmtVmsString;

   if (VMSnok (status =
       sys$bintim (&TimeGmtVmsStringDsc, &TimeGmtDeltaBinary)))
      return (status);
   if (TimeGmtDeltaBinary[0] || TimeGmtDeltaBinary[1])
      return (status);
   /* time must have been zero, make it one, one-hundreth of a second */
   TimeGmtDeltaBinary[0] = -100000;
   TimeGmtDeltaBinary[1] = -1;
   return (SS$_NORMAL);
}

/*****************************************************************************/
/*
The GMT is generated by calculating using an offset using 
'TimeGmtDeltaOffset' and boolean 'TimeAheadOfGmt'.  Adjust either to or from 
GMT.
*/ 

int TimeAdjustGMT
(
BOOL ToGmTime,
unsigned long *BinTimePtr
)
{
   int  status;
   unsigned long  AdjustedTime [2];

   /*********/
   /* begin */
   /*********/

   if (Debug) fprintf (stdout, "TimeAdjustGMT() ToGmTime: %d\n", ToGmTime);

   if ((ToGmTime && TimeAheadOfGmt) || (!ToGmTime && !TimeAheadOfGmt))
   {
      /* to GMT from local and ahead of GMT, or to local from GMT and behind */
      status = lib$sub_times (BinTimePtr, &TimeGmtDeltaBinary, &AdjustedTime);
      if (Debug) fprintf (stdout, "lib$sub_times() %%X%08.08X\n", status);
   }
   else
   {
      /* to GMT from local and behind GMT, or to local from GMT and ahead */
      status = lib$add_times (BinTimePtr, &TimeGmtDeltaBinary, &AdjustedTime);
      if (Debug) fprintf (stdout, "lib$add_times() %%X%08.08X\n", status);
   }

   if (Debug)
   {
      unsigned short  Length;
      char  String [64];
      $DESCRIPTOR (AdjustedTimeFaoDsc, "AdjustedTime: |!%D|\n");
      $DESCRIPTOR (StringDsc, String);

      sys$fao (&AdjustedTimeFaoDsc, &Length, &StringDsc, &AdjustedTime);
      String[Length] = '\0';
      fputs (String, stdout);
   }

   BinTimePtr[0] = AdjustedTime[0];
   BinTimePtr[1] = AdjustedTime[1];

   return (status);
}

/****************************************************************************/
/*
Return an integer reflecting the major and minor version of VMS (e.g. 60, 61,
62, 70, 71, 72, etc.)
*/ 

#ifdef ODS_EXTENDED

int GetVmsVersion ()

{
   static char  SyiVersion [16];

   static struct {
      short int  buf_len;
      short int  item;
      void  *buf_addr;
      unsigned short  *ret_len;
   }
   SyiItems [] =
   {
      { 8, SYI$_VERSION, &SyiVersion, 0 },
      { 0,0,0,0 }
   };

   int  status,
        version;

   /*********/
   /* begin */
   /*********/

   if (Debug) fprintf (stdout, "GetVmsVersion()\n");

   if (VMSnok (status = sys$getsyiw (0, 0, 0, &SyiItems, 0, 0, 0)))
      exit (status);
   SyiVersion[8] = '\0';
   version = ((SyiVersion[1]-48) * 10) + (SyiVersion[3]-48);
   if (Debug) fprintf (stdout, "|%s| %d\n", SyiVersion, version);
   return (version);
}

#endif /* ODS_EXTENDED */

/*****************************************************************************/
/*
Return non-NULL pointer if looks a bit suspect!
*/

char* XSSuspect (char *cptr)

{
   static char  XSSredacted [] = "XSS redacted! (tsk-tsk)";

   char  *sptr, *zptr;
   char buf [1024];

   /*********/
   /* begin */
   /*********/

   if (Debug) fprintf (stdout, "XSSuspect() |%s|\n", cptr);

   if (!cptr || !*cptr) return (NULL);
   if (strstr (cptr, "</")) return (XSSredacted);
   zptr = (sptr = buf) + sizeof(buf)-1;
   while (*cptr && *cptr && sptr < zptr) *sptr++ = tolower(*cptr++);
   *sptr = '\0';
   if (sptr >= zptr || strstr (buf, "<script>")) return (XSSredacted);
   return (NULL);
}

/****************************************************************************/
/*
Does a case-insensitive, character-by-character string compare and returns 
true if two strings are the same, or false if not.  If a maximum number of 
characters are specified only those will be compared, if the entire strings 
should be compared then specify the number of characters as 0.
*/ 
 
BOOL strsame
(
char *sptr1,
char *sptr2,
int  count
)
{
   while (*sptr1 && *sptr2)
   {
      if (toupper (*sptr1++) != toupper (*sptr2++)) return (false);
      if (count)
         if (!--count) return (true);
   }
   if (*sptr1 || *sptr2)
      return (false);
   else
      return (true);
}
 
/*****************************************************************************/

