// -*- C++ -*-
/* Copyright (C) 1989, 1990, 1991, 1992 Free Software Foundation, Inc.
     Written by James Clark (jjc@jclark.com)

This file is part of groff.

groff is free software; you can redistribute it and/or modify it under
the terms of the GNU General Public License as published by the Free
Software Foundation; either version 2, or (at your option) any later
version.

groff is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or
FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
for more details.

You should have received a copy of the GNU General Public License along
with groff; see the file COPYING.  If not, write to the Free Software
Foundation, 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */

#include "driver.h"
#include "stringclass.h"
#include "cset.h"

#include "ps.h"

#define PROLOGUE "prologue"

static void print_ps_string(const string &s, FILE *outfp);

cset white_space("\n\r \t");
string an_empty_string;

const char *extension_table[] = {
  "DPS",
  "CMYK",
  "Composite",
  "FileSystem",
};

const int NEXTENSIONS = sizeof(extension_table)/sizeof(extension_table[0]);

const char *resource_table[] = {
  "font",
  "procset",
  "file",
  "encoding",
  "form",
  "pattern",
};

const int NRESOURCES = sizeof(resource_table)/sizeof(resource_table[0]);

struct resource {
  resource *next;
  resource_type type;
  string name;
  enum { NEEDED = 01, SUPPLIED = 02, FONT_NEEDED = 04, BUSY = 010 };
  unsigned flags;
  string version;
  unsigned revision;
  char *filename;
  int rank;
  resource(resource_type, string &, string & = an_empty_string, unsigned = 0);
  ~resource();
  void print_type_and_name(FILE *outfp);
};

resource::resource(resource_type t, string &n, string &v, unsigned r)
: type(t), revision(r), flags (0), filename(0), rank(-1), next(0)
{
  name.move(n);
  version.move(v);
  if (type == RESOURCE_FILE) {
    if (name.search('\0') >= 0)
      error("filename contains a character with code 0");
    filename = name.extract();
  }
}

resource::~resource()
{
  a_delete filename;
}

void resource::print_type_and_name(FILE *outfp)
{
  fputs(resource_table[type], outfp);
  putc(' ', outfp);
  print_ps_string(name, outfp);
  if (type == RESOURCE_PROCSET) {
    putc(' ', outfp);
    print_ps_string(version, outfp);
    fprintf(outfp, " %u", revision);
  }
}

resource_manager::resource_manager()
: resource_list(0), extensions(0), language_level(0)
{
  read_download_file();
  string procset_name("grops");
  extern const char *version_string;
  string procset_version(version_string);
  procset_resource = lookup_resource(RESOURCE_PROCSET, procset_name,
				     procset_version, 0);
  procset_resource->flags |= resource::SUPPLIED;
}

resource_manager::~resource_manager()
{
  while (resource_list) {
    resource *tem = resource_list;
    resource_list = resource_list->next;
    delete tem;
  }
}

resource *resource_manager::lookup_resource(resource_type type,
					    string &name,
					    string &version,
					    unsigned revision)
{
  resource *r;
  for (r = resource_list; r; r = r->next)
    if (r->type == type
	&& r->name == name
	&& r->version == version
	&& r->revision == revision)
      return r;
  r = new resource(type, name, version, revision);
  r->next = resource_list;
  resource_list = r;
  return r;
}

// Just a specialized version of lookup_resource().

resource *resource_manager::lookup_font(const char *name)
{
  string dummy("");
  resource *r;
  for (r = resource_list; r; r = r->next)
    if (r->type == RESOURCE_FONT
	&& strlen(name) == r->name.length()
	&& memcmp(name, r->name.contents(), r->name.length()) == 0)
      return r;
  string s(name);
  r = new resource(RESOURCE_FONT, s, dummy);
  r->next = resource_list;
  resource_list = r;
  return r;
}

void resource_manager::need_font(const char *name)
{
  lookup_font(name)->flags |= resource::FONT_NEEDED;
}

typedef resource *Presource;	// Work around g++ bug.

void resource_manager::document_setup(ps_output &out)
{
  int nranks = 0;
  resource *r;
  for (r = resource_list; r; r = r->next)
    if (r->rank >= nranks)
      nranks = r->rank + 1;
  if (nranks > 0) {
    // Sort resource_list in reverse order of rank.
    Presource *head = new Presource[nranks + 1];
    Presource **tail = new Presource *[nranks + 1];
    int i;
    for (i = 0; i < nranks + 1; i++) {
      head[i] = 0;
      tail[i] = &head[i];
    }
    for (r = resource_list; r; r = r->next) {
      i = r->rank < 0 ? 0 : r->rank + 1;
      *tail[i] = r;
      tail[i] = &(*tail[i])->next;
    }
    resource_list = 0;
    for (i = 0; i < nranks + 1; i++)
      if (head[i]) {
	*tail[i] = resource_list;
	resource_list = head[i];
      }
    a_delete head;
    a_delete tail;
    // check it
    for (r = resource_list; r; r = r->next)
      if (r->next)
	assert(r->rank >= r->next->rank);
    for (r = resource_list; r; r = r->next)
      if (r->type == RESOURCE_FONT && r->rank >= 0)
	supply_resource(r, -1, out.get_file());
  }
}

void resource_manager::print_resources_comment(unsigned flag, FILE *outfp)
{
  int continued = 0;
  for (resource *r = resource_list; r; r = r->next)
    if (r->flags & flag) {
      if (continued)
	fputs("%%+ ", outfp);
      else {
	fputs(flag == resource::NEEDED
	      ? "%%DocumentNeededResources: "
	      : "%%DocumentSuppliedResources: ",
	      outfp);
	continued = 1;
      }
      r->print_type_and_name(outfp);
      putc('\n', outfp);
    }
}

void resource_manager::print_header_comments(ps_output &out)
{
  for (resource *r = resource_list; r; r = r->next)
    if (r->type == RESOURCE_FONT && (r->flags & resource::FONT_NEEDED))
      supply_resource(r, 0, 0);
  print_resources_comment(resource::NEEDED, out.get_file());
  print_resources_comment(resource::SUPPLIED, out.get_file());
  print_language_level_comment(out.get_file());
  print_extensions_comment(out.get_file());
}

void resource_manager::output_prolog(ps_output &out)
{
  FILE *outfp = out.get_file();
  out.end_line();
  char *path;
  FILE *fp = font::open_file(PROLOGUE, &path);
  if (!fp)
    fatal("can't find `%1'", PROLOGUE);
  fputs("%%BeginResource: ", outfp);
  procset_resource->print_type_and_name(outfp);
  putc('\n', outfp);
  process_file(-1, fp, path, outfp);
  fclose(fp);
  a_delete path;
  fputs("%%EndResource\n", outfp);
}

void resource_manager::import_file(const char *filename, ps_output &out)
{
  out.end_line();
  string name(filename);
  resource *r = lookup_resource(RESOURCE_FILE, name);
  supply_resource(r, -1, out.get_file(), 1);
}

void resource_manager::supply_resource(resource *r, int rank, FILE *outfp,
				       int is_document)
{
  if (r->flags & resource::BUSY) {
    r->name += '\0';
    fatal("loop detected in dependency graph for %1 `%2'",
	  resource_table[r->type],
	  r->name.contents());
  }
  r->flags |= resource::BUSY;
  if (rank > r->rank)
    r->rank = rank;
  char *path;
  FILE *fp = 0;
  if (r->filename != 0) {
    if (r->type == RESOURCE_FONT) {
      fp = font::open_file(r->filename, &path);
      if (!fp) {
	error("can't find `%1'", r->filename);
	a_delete r->filename;
	r->filename = 0;
      }
    }
    else {
      errno = 0;
      fp = fopen(r->filename, "r");
      if (!fp) {
	error("can't open `%1': %2", r->filename, strerror(errno));
	a_delete r->filename;
	r->filename = 0;
      }
      else
	path = r->filename;
    }
  }
  if (fp) {
    if (outfp) {
      if (r->type == RESOURCE_FILE && is_document) {
	fputs("%%BeginDocument: ", outfp);
	print_ps_string(r->name, outfp);
	putc('\n', outfp);
      }
      else {
	fputs("%%BeginResource: ", outfp);
	r->print_type_and_name(outfp);
	putc('\n', outfp);
      }
    }
    process_file(rank, fp, path, outfp);
    fclose(fp);
    if (r->type == RESOURCE_FONT)
      a_delete path;
    if (outfp) {
      if (r->type == RESOURCE_FILE && is_document)
	fputs("%%EndDocument\n", outfp);
      else
	fputs("%%EndResource\n", outfp);
    }
    r->flags |= resource::SUPPLIED;
  }
  else {
    if (outfp) {
      if (r->type == RESOURCE_FILE && is_document) {
	fputs("%%IncludeDocument: ", outfp);
	print_ps_string(r->name, outfp);
	putc('\n', outfp);
      }
      else {
	fputs("%%IncludeResource: ", outfp);
	r->print_type_and_name(outfp);
	putc('\n', outfp);
      }
    }
    r->flags |= resource::NEEDED;
  }
  r->flags &= ~resource::BUSY;
}


#define PS_LINE_MAX 255
#define PS_MAGIC "%!PS-Adobe-"

static int ps_get_line(char *buf, FILE *fp)
{
  int c = getc(fp);
  if (c == EOF) {
    buf[0] = '\0';
    return 0;
  }
  current_lineno++;
  int i = 0;
  int err = 0;
  while (c != '\r' && c != '\n' && c != EOF) {
    if ((c < 0x1b && !white_space(c)) || c == 0x7f)
      error("illegal input character code %1", int(c));
    else if (i < PS_LINE_MAX)
      buf[i++] = c;
    else if (!err) {
      err = 1;
      error("PostScript file non-conforming "
	    "because length of line exceeds 255");
    }
    c = getc(fp);
  }
  buf[i++] = '\n';
  buf[i] = '\0';
  if (c == '\r') {
    c = getc(fp);
    if (c != EOF && c != '\n')
      ungetc(c, fp);
  }
  return 1;
}

static int read_text_arg(const char **pp, string &res)
{
  res.clear();
  while (white_space(**pp))
    *pp += 1;
  if (**pp == '\0') {
    error("missing argument");
    return 0;
  }
  if (**pp != '(') {
    for (; **pp != '\0' && !white_space(**pp); *pp += 1)
      res += **pp;
    return 1;
  }
  *pp += 1;
  res.clear();
  int level = 0;
  for (;;) {
    if (**pp == '\0' || **pp == '\r' || **pp == '\n') {
      error("missing ')'");
      return 0;
    }
    if (**pp == ')') {
      if (level == 0) {
	*pp += 1;
	break;
      }
      res += **pp;
      level--;
    }
    else if (**pp == '(') {
      level++;
      res += **pp;
    }
    else if (**pp == '\\') {
      *pp += 1;
      switch (**pp) {
      case 'n':
	res += '\n';
	break;
      case 'r':
	res += '\n';
	break;
      case 't':
	res += '\t';
	break;
      case 'b':
	res += '\b';
	break;
      case 'f':
	res += '\f';
	break;
      case '0':
      case '1':
      case '2':
      case '3':
      case '4':
      case '5':
      case '6':
      case '7':
	{
	  int val = **pp - '0';
	  if ((*pp)[1] >= '0' && (*pp)[1] <= '7') {
	    *pp += 1;
	    val = val*8 + (**pp - '0');
	    if ((*pp)[1] >= '0' && (*pp)[1] <= '7') {
	      *pp += 1;
	      val = val*8 + (**pp - '0');
	    }
	  }
	}
	break;
      default:
	res += **pp;
	break;
      }
    }
    else
      res += **pp;
    *pp += 1;
  }
  return 1;
}

static int read_uint_arg(const char **pp, unsigned *res)
{
  while (white_space(**pp))
    *pp += 1;
  if (**pp == '\0') {
    error("missing argument");
    return 0;
  }
  const char *start = *pp;
  // XXX use strtoul
  long n = strtol(start, (char **)pp, 10);
  if (n == 0 && *pp == start) {
    error("not an integer");
    return 0;
  }
  if (n < 0) {
    error("argument must not be negative");
    return 0;
  }
  *res = unsigned(n);
  return 1;
}

resource *resource_manager::read_file_arg(const char **ptr)
{
  string arg;
  if (!read_text_arg(ptr, arg))
    return 0;
  return lookup_resource(RESOURCE_FILE, arg);
}

resource *resource_manager::read_font_arg(const char **ptr)
{
  string arg;
  if (!read_text_arg(ptr, arg))
    return 0;
  return lookup_resource(RESOURCE_FONT, arg);
}

resource *resource_manager::read_procset_arg(const char **ptr)
{
  string arg;
  if (!read_text_arg(ptr, arg))
    return 0;
  string version;
  if (!read_text_arg(ptr, version))
      return 0;
  unsigned revision;
  if (!read_uint_arg(ptr, &revision))
      return 0;
  return lookup_resource(RESOURCE_PROCSET, arg, version, revision);
}

resource *resource_manager::read_resource_arg(const char **ptr)
{
  while (white_space(**ptr))
    *ptr += 1;
  const char *name = *ptr;
  while (**ptr != '\0' && !white_space(**ptr))
    *ptr += 1;
  if (name == *ptr) {
    error("missing resource type");
    return 0;
  }
  int ri;
  for (ri = 0; ri < NRESOURCES; ri++)
    if (strlen(resource_table[ri]) == *ptr - name
	&& memcmp(resource_table[ri], name, *ptr - name) == 0)
      break;
  if (ri >= NRESOURCES) {
    error("unknown resource type");
    return 0;
  }
  if (ri == RESOURCE_PROCSET)
    return read_procset_arg(ptr);
  string arg;
  if (!read_text_arg(ptr, arg))
    return 0;
  return lookup_resource(resource_type(ri), arg);
}

static const char *matches_comment(const char *buf, const char *comment)
{
  if (buf[0] != '%' || buf[1] != '%')
    return 0;
  for (buf += 2; *comment; comment++, buf++)
    if (*buf != *comment)
      return 0;
  if (comment[-1] == ':')
    return buf;
  if (*buf == '\0' || white_space(*buf))
    return buf;
  return 0;
}

// Return 1 if the line should be copied out.

int resource_manager::do_begin_resource(const char *ptr, int, FILE *,
					FILE *)
{
  resource *r = read_resource_arg(&ptr);
  if (r)
    r->flags |= resource::SUPPLIED;
  return 1;
}

int resource_manager::do_include_resource(const char *ptr, int rank, FILE *,
					  FILE *outfp)
{
  resource *r = read_resource_arg(&ptr);
  if (r) {
    if (r->type == RESOURCE_FONT) {
      if (rank >= 0)
	supply_resource(r, rank + 1, outfp);
      else
	r->flags |= resource::FONT_NEEDED;
    }
    else
      supply_resource(r, rank, outfp);
  }
  return 0;
}

int resource_manager::do_begin_document(const char *ptr, int, FILE *,
					FILE *)
{
  resource *r = read_file_arg(&ptr);
  if (r)
    r->flags |= resource::SUPPLIED;
  return 1;
}

int resource_manager::do_include_document(const char *ptr, int rank, FILE *,
					  FILE *outfp)
{
  resource *r = read_file_arg(&ptr);
  if (r)
    supply_resource(r, rank, outfp, 1);
  return 0;
}

int resource_manager::do_begin_procset(const char *ptr, int, FILE *,
				       FILE *outfp)
{
  resource *r = read_procset_arg(&ptr);
  if (r) {
    r->flags |= resource::SUPPLIED;
    if (outfp) {
      fputs("%%BeginResource: ", outfp);
      r->print_type_and_name(outfp);
      putc('\n', outfp);
    }
  }
  return 0;
}

int resource_manager::do_include_procset(const char *ptr, int rank, FILE *,
					  FILE *outfp)
{
  resource *r = read_procset_arg(&ptr);
  if (r)
    supply_resource(r, rank, outfp);
  return 0;
}

int resource_manager::do_begin_file(const char *ptr, int, FILE *,
				    FILE *outfp)
{
  resource *r = read_file_arg(&ptr);
  if (r) {
    r->flags |= resource::SUPPLIED;
    if (outfp) {
      fputs("%%BeginResource: ", outfp);
      r->print_type_and_name(outfp);
      putc('\n', outfp);
    }
  }
  return 0;
}

int resource_manager::do_include_file(const char *ptr, int rank, FILE *,
				      FILE *outfp)
{
  resource *r = read_file_arg(&ptr);
  if (r)
    supply_resource(r, rank, outfp);
  return 0;
}

int resource_manager::do_begin_font(const char *ptr, int, FILE *,
				    FILE *outfp)
{
  resource *r = read_font_arg(&ptr);
  if (r) {
    r->flags |= resource::SUPPLIED;
    if (outfp) {
      fputs("%%BeginResource: ", outfp);
      r->print_type_and_name(outfp);
      putc('\n', outfp);
    }
  }
  return 0;
}

int resource_manager::do_include_font(const char *ptr, int rank, FILE *,
				      FILE *outfp)
{
  resource *r = read_font_arg(&ptr);
  if (r) {
    if (rank >= 0)
      supply_resource(r, rank + 1, outfp);
    else
      r->flags |= resource::FONT_NEEDED;
  }
  return 0;
}

int resource_manager::change_to_end_resource(const char *, int, FILE *,
					     FILE *outfp)
{
  if (outfp)
    fputs("%%EndResource\n", outfp);
  return 0;
}

int resource_manager::do_begin_preview(const char *, int, FILE *fp, FILE *)
{
  char buf[PS_LINE_MAX + 2];
  do {
    if (!ps_get_line(buf, fp)) {
      error("end of file in preview section");
      break;
    }
  } while (!matches_comment(buf, "EndPreview"));
  return 0;
}

int read_one_of(const char **ptr, const char **s, int n)
{
  while (white_space(**ptr))
    *ptr += 1;
  if (**ptr == '\0')
    return -1;
  const char *start = *ptr;
  do {
    ++ptr;
  } while (**ptr != '\0' && !white_space(**ptr));
  for (int i = 0; i < n; i++)
    if (strlen(s[i]) == *ptr - start
	&& memcmp(s[i], start, *ptr - start) == 0)
      return i;
  return -1;
}

int resource_manager::do_begin_data(const char *ptr, int, FILE *fp,
				    FILE *outfp)
{
  while (white_space(*ptr))
    ptr++;
  const char *start = ptr;
  unsigned numberof;
  if (!read_uint_arg(&ptr, &numberof))
    return 0;
  static const char *types[] = { "Binary", "Hex", "ASCII" };
  const int Binary = 0;
  int type = 0;
  static const char *units[] = { "Bytes", "Lines" };
  const int Bytes = 0;
  int unit = Bytes;
  while (white_space(*ptr))
    ptr++;
  if (*ptr != '\0') {
    type = read_one_of(&ptr, types, 3);
    if (type < 0) {
      error("bad data type");
      return 0;
    }
    while (white_space(*ptr))
      ptr++;
    if (*ptr != '\0') {
      unit = read_one_of(&ptr, units, 2);
      if (unit < 0) {
	error("expected `Bytes' or `Lines'");
	return 0;
      }
    }
  }
  if (type != Binary)
    return 1;
  if (outfp) {
    fputs("%%BeginData: ", outfp);
    fputs(start, outfp);
  }
  if (numberof > 0) {
    unsigned bytecount = 0;
    unsigned linecount = 0;
    do {
      int c = getc(fp);
      if (c == EOF) {
	error("end of file within data section");
	return 0;
      }
      if (outfp)
	putc(c, outfp);
      bytecount++;
      if (c == '\r') {
	int cc = getc(fp);
	if (cc != '\n') {
	  linecount++;
	  current_lineno++;
	}
	if (cc != EOF)
	  ungetc(c, fp);
      }
      else if (c == '\n') {
	linecount++;
	current_lineno++;
      }
    } while ((unit == Bytes ? bytecount : linecount) < numberof);
  }
  char buf[PS_LINE_MAX + 2];
  if (!ps_get_line(buf, fp)) {
    error("missing %%%%EndData line");
    return 0;
  }
  if (!matches_comment(buf, "EndData"))
    error("bad %%%%EndData line");
  if (outfp)
    fputs(buf, outfp);
  return 0;
}

int resource_manager::do_begin_binary(const char *ptr, int, FILE *fp,
				      FILE *outfp)
{
  if (!outfp)
    return 0;
  unsigned count;
  if (!read_uint_arg(&ptr, &count))
    return 0;
  if (outfp)
    fprintf(outfp, "%%%%BeginData: %u Binary Bytes\n", count);
  while (count != 0) {
    int c = getc(fp);
    if (c == EOF) {
      error("end of file within binary section");
      return 0;
    }
    if (outfp)
      putc(c, outfp);
    --count;
    if (c == '\r') {
      int cc = getc(fp);
      if (cc != '\n')
	current_lineno++;
      if (cc != EOF)
	ungetc(c, fp);
    }
    else if (c == '\n')
      current_lineno++;
  }
  char buf[PS_LINE_MAX + 2];
  if (!ps_get_line(buf, fp)) {
    error("missing %%%%EndBinary line");
    return 0;
  }
  if (!matches_comment(buf, "EndBinary")) {
    error("bad %%%%EndBinary line");
    if (outfp)
      fputs(buf, outfp);
  }
  else if (outfp)
    fputs("%%EndData\n", outfp);
  return 0;
}

static unsigned parse_extensions(const char *ptr)
{
  unsigned flags = 0;
  for (;;) {
    while (white_space(*ptr))
      ptr++;
    if (*ptr == '\0')
      break;
    const char *name = ptr;
    do {
      ++ptr;
    } while (*ptr != '\0' && !white_space(*ptr));
    int i;
    for (i = 0; i < NEXTENSIONS; i++)
      if (strlen(extension_table[i]) == ptr - name
	  && memcmp(extension_table[i], name, ptr - name) == 0) {
	flags |= (1 << i);
	break;
      }
    if (i >= NEXTENSIONS) {
      string s(name, ptr - name);
      s += '\0';
      error("unknown extension `%1'", s.contents());
    }
  }
  return flags;
}

// XXX if it has not been surrounded with {Begin,End}Document need to strip
// out Page: Trailer {Begin,End}Prolog {Begin,End}Setup sections.

// XXX Perhaps the decision whether to use BeginDocument or
// BeginResource: file should be postponed till we have seen
// the first line of the file.

void resource_manager::process_file(int rank, FILE *fp, const char *filename,
				    FILE *outfp)
{
  // If none of these comments appear in the header section, and we are
  // just analyzing the file (ie outfp is 0), then we can return immediately.
  static const char *header_comment_table[] = {
    "DocumentNeededResources:",
    "DocumentSuppliedResources:",
    "DocumentNeededFonts:",
    "DocumentSuppliedFonts:",
    "DocumentNeededProcSets:",
    "DocumentSuppliedProcSets:",
    "DocumentNeededFiles:",
    "DocumentSuppliedFiles:",
  };
  
  const int NHEADER_COMMENTS = (sizeof(header_comment_table)
				/ sizeof(header_comment_table[0]));
  struct comment_info {
    const char *name;
    int (resource_manager::*proc)(const char *, int, FILE *, FILE *);
  };

  static comment_info comment_table[] = {
    { "BeginResource:", &resource_manager::do_begin_resource },
    { "IncludeResource:", &resource_manager::do_include_resource },
    { "BeginDocument:", &resource_manager::do_begin_document },
    { "IncludeDocument:", &resource_manager::do_include_document },
    { "BeginProcSet:", &resource_manager::do_begin_procset },
    { "IncludeProcSet:", &resource_manager::do_include_procset },
    { "BeginFont:", &resource_manager::do_begin_font },
    { "IncludeFont:", &resource_manager::do_include_font },
    { "BeginFile:", &resource_manager::do_begin_file },
    { "IncludeFile:", &resource_manager::do_include_file },
    { "EndProcSet", &resource_manager::change_to_end_resource },
    { "EndFont", &resource_manager::change_to_end_resource },
    { "EndFile", &resource_manager::change_to_end_resource },
    { "BeginPreview:", &resource_manager::do_begin_preview },
    { "BeginData:", &resource_manager::do_begin_data },
    { "BeginBinary:", &resource_manager::do_begin_binary },
  };
  
  const int NCOMMENTS = sizeof(comment_table)/sizeof(comment_table[0]);
  char buf[PS_LINE_MAX + 2];
  int saved_lineno = current_lineno;
  const char *saved_filename = current_filename;
  current_filename = filename;
  current_lineno = 0;
  if (!ps_get_line(buf, fp)) {
    current_filename = saved_filename;
    current_lineno = saved_lineno;
    return;
  }
  if (strlen(buf) < sizeof(PS_MAGIC) - 1
      || memcmp(buf, PS_MAGIC, sizeof(PS_MAGIC) - 1) != 0) {
    if (outfp) {
      do {
	if (!(broken_flags & STRIP_PERCENT_BANG)
	    || buf[0] != '%' || buf[1] != '!')
	  fputs(buf, outfp);
      } while (ps_get_line(buf, fp));
    }
  }
  else {
    if (!(broken_flags & STRIP_PERCENT_BANG) && outfp)
      fputs(buf, outfp);
    int in_header = 1;
    int interesting = 0;
    int had_extensions_comment = 0;
    int had_language_level_comment = 0;
    for (;;) {
      if (!ps_get_line(buf, fp))
	break;
      int copy_this_line = 1;
      if (buf[0] == '%') {
	if (buf[1] == '%') {
	  const char *ptr;
	  int i;
	  for (i = 0; i < NCOMMENTS; i++)
	    if (ptr = matches_comment(buf, comment_table[i].name)) {
	      copy_this_line
		= (this->*(comment_table[i].proc))(ptr, rank, fp, outfp);
	      break;
	    }
	  if (i >= NCOMMENTS && in_header) {
	    if (ptr = matches_comment(buf, "EndComments"))
	      in_header = 0;
	    else if (!had_extensions_comment
		     && (ptr = matches_comment(buf, "Extensions:"))) {
	      extensions |= parse_extensions(ptr);
	      // XXX handle possibility that next line is %%+
	      had_extensions_comment = 1;
	    }
	    else if (!had_language_level_comment
		     && (ptr = matches_comment(buf, "LanguageLevel:"))) {
	      unsigned ll;
	      if (read_uint_arg(&ptr, &ll) && ll > language_level)
		language_level = ll;
	      had_language_level_comment = 1;
	    }
	    else {
	      for (i = 0; i < NHEADER_COMMENTS; i++)
		if (matches_comment(buf, header_comment_table[i])) {
		  interesting = 1;
		  break;
		}
	    }
	  }
	  if ((broken_flags & STRIP_STRUCTURE_COMMENTS)
	      && (matches_comment(buf, "EndProlog")
		  || matches_comment(buf, "Page:")
		  || matches_comment(buf, "Trailer")))
	    copy_this_line = 0;
	}
	else if (buf[1] == '!') {
	  if (broken_flags & STRIP_PERCENT_BANG)
	    copy_this_line = 0;
	}
      }
      else
	in_header = 0;
      if (!outfp && !in_header && !interesting)
	break;
      if (copy_this_line && outfp)
	fputs(buf, outfp);
    }
  }
  current_filename = saved_filename;
  current_lineno = saved_lineno;
}

void resource_manager::read_download_file()
{
  char *path = 0;
  FILE *fp = font::open_file("download", &path);
  if (!fp)
    fatal("can't find `download'");
  char buf[512];
  int lineno = 0;
  while (fgets(buf, sizeof(buf), fp)) {
    lineno++;
    char *p = strtok(buf, " \t\r\n");
    if (p == 0 || *p == '#')
      continue;
    char *q = strtok(0, " \t\r\n");
    if (!q)
      fatal_with_file_and_line(path, lineno, "missing filename");
    lookup_font(p)->filename = strsave(q);
  }
  a_delete path;
  fclose(fp);
}

// XXX Can we share some code with ps_output::put_string()?

static void print_ps_string(const string &s, FILE *outfp)
{
  int len = s.length();
  const char *str = s.contents();
  int funny = 0;
  if (str[0] == '(')
    funny = 1;
  else {
    for (int i = 0; i < len; i++)
      if (str[i] <= 040 || str[i] > 0176) {
	funny = 1;
	break;
      }
  }
  if (!funny) {
    put_string(s, outfp);
    return;
  }
  int level = 0;
  int i;
  for (i = 0; i < len; i++)
    if (str[i] == '(')
      level++;
    else if (str[i] == ')' && --level < 0)
      break;
  putc('(', outfp);
  for (i = 0; i < len; i++)
    switch (str[i]) {
    case '(':
    case ')':
      if (level != 0)
	putc('\\', outfp);
      putc(str[i], outfp);
      break;
    case '\\':
      fputs("\\\\", outfp);
      break;
    case '\n':
      fputs("\\n", outfp);
      break;
    case '\r':
      fputs("\\r", outfp);
      break;
    case '\t':
      fputs("\\t", outfp);
      break;
    case '\b':
      fputs("\\b", outfp);
      break;
    case '\f':
      fputs("\\f", outfp);
      break;
    default:
      if (str[i] < 040 || str[i] > 0176)
	fprintf(outfp, "\\%03o", str[i] & 0377);
      else
	putc(str[i], outfp);
      break;
    }
  putc(')', outfp);
}

void resource_manager::print_extensions_comment(FILE *outfp)
{
  if (extensions) {
    fputs("%%Extensions:", outfp);
    for (int i = 0; i < NEXTENSIONS; i++)
      if (extensions & (1 << i)) {
	putc(' ', outfp);
	fputs(extension_table[i], outfp);
      }
    putc('\n', outfp);
  }
}

void resource_manager::print_language_level_comment(FILE *outfp)
{
  if (language_level)
    fprintf(outfp, "%%%%LanguageLevel: %u\n", language_level);
}

