#pike __REAL_VERSION__

//! @appears Image.PS
//! Codec for the Adobe page description language PostScript.
//! Uses Ghostscript for decoding or built-in support.

protected string find_in_path( string file )
{
  string path=getenv("PATH");
  foreach(path ? path/":" : ({}) , path)
    if(file_stat(path=combine_path(path,file)))
      return path;
}

//! Decodes the postscript @[data] into an image object
//! using Ghostscript.
//! @param options
//!   Optional decoding parameters.
//!   @mapping
//!     @member int "dpi"
//!       The resolution the image should be rendered in.
//!       Defaults to 100.
//!     @member string "device"
//!       The selected Ghostscript device. Defaults to "ppmraw".
//!     @member string "binary"
//!       Path to the Ghostscript binary to be used. If missing
//!       the environment paths will be searched for a file "gs"
//!       to be used instead.
//!     @member int(0..1) "force_gs"
//!	   Forces use of Ghostscript for EPS files instead
//!	   of Pikes native support.
//!	@member int(0..1) "eps_crop"
//!	   Use -dEPSCrop option to Ghostscript to crop the
//!	   BoundingBox for a EPS file.
//!	@member int(0..1) "cie_color"
//!	   Use -dUseCIEColor option to Ghostscript for
//!	   mapping color values through a CIE color space.
//!     @member string "file"
//!        Filename to read. If this is specified, it will be
//!        passed along to the @tt{gs@} binary, so that it can
//!        read the file directly. If this is specified @[data]
//!        may be set to @expr{0@} (zero).
//!   @endmapping
//!
//! @note
//!   Some versions of @tt{gs@} on MacOS X have problems with
//!   reading files on stdin. If this occurrs, try writing to
//!   a plain file and specifying the @tt{file@} option.
//!
//! @note
//!   @tt{gs@} versions 7.x and earlier don't support rendering
//!   of EPSes if they are specified with the @tt{file@} option.
//!   If this is a problem, upgrade to @tt{gs@} version 8.x or
//!   later.
object decode( string data, mapping|void options )
{
  if(!options) options = ([]);
  if (data) {
    if(has_prefix(data, "\xc5\xd0\xd3\xc6")) {
      // DOS EPS Binary Header.
      int ps_start, ps_len, meta_start, meta_len, tiff_start, tiff_len, sum;
      sscanf(data, "%*4c%-4c%-4c%-4c%-4c%-4c%-4c%-2c",
	     ps_start, ps_len, meta_start, meta_len,
	     tiff_start, tiff_len, sum);
#if constant(Image.TIFF.decode)
      if (tiff_start && tiff_len) {
	return Image.TIFF.decode(data[tiff_start..tiff_start + tiff_len-1]);
      }
#endif
      data = data[ps_start..ps_start+ps_len-1];
    }
    if(data[0..3] != "%!PS")
      error("This is not a postscript file!\n");

    if(!options->force_gs)
    {
      if (has_prefix(data, "%!PS-Adobe-3.0 EPSF-3.0")) {
	int width, height, bits, ncols;
	int nbws, width2, encoding;
	string init_tag;
	if ((sscanf(data,
		    "%*s%%ImageData:%*[ ]%d%*[ ]%d%*[ ]%d%*[ ]%d%"
		    "*[ ]%d%*[ ]%d%*[ ]%d%*[ ]\"%s\"",
		    width, height, bits, ncols,
		    nbws, width2, encoding, init_tag) > 7) &&
	    (width == width2) && (width > 0) && (height > 0) && (bits == 8) &&
	    (< 1, 2 >)[encoding]) {
	  // Image data present.
	  int len;
	  string term;
	  string raw;
	  if ((sscanf(data, "%*s%%%%BeginBinary:%*[ ]%d%[\r\n]%s",
		      len, term, raw) == 5) &&
	      (len>0) && has_prefix(raw, init_tag + term)) {
	    raw = raw[sizeof(init_tag+term)..len-1];
	    if (encoding == 2) {
	      raw = String.hex2string(raw - term);
	    }
	    if (sizeof(raw) == width*height*(ncols+nbws)) {
	      array(string) rows = raw/width;
	      if ((<3,4>)[ncols]) {
		// RGB or CMYK image.
		array(string) channels = allocate(ncols, "");
		int c;
		for (c = 0; c < ncols; c++) {
		  int i;
		  for (i = c; i < sizeof(rows); i += ncols+nbws) {
		    channels[c] += rows[i];
		  }
		}
		if (ncols == 3) {
		  return Image.Image(width, height, "rgb", @channels);
		}
		return Image.Image(width, height, "adjusted_cmyk", @channels);
	      }
	      string grey = "";
	      int i;
	      for(i = ncols; i < sizeof(rows); i += ncols+nbws) {
		grey += rows[i];
	      }
	      return Image.Image(width, height, "rgb", grey, grey, grey);
	    }
	  }
	}
      }
    }
  }

  Stdio.File fd = Stdio.File();
  object fd2 = fd->pipe();

  Stdio.File fd3 = Stdio.File();
  object fd4 = fd3->pipe();

  array command = ({
    options->binary||find_in_path("gs")||("/bin/sh -c gs "),
    "-quiet",
    "-sDEVICE="+(options->device||"ppmraw"),
    "-r"+(options->dpi||100),
    "-dBATCH",
    "-dNOPAUSE"});

  if(options->eps_crop)
    command += ({"-dEPSCrop"});

  if(options->cie_color)
    command += ({"-dUseCIEColor"});

  command += ({
    "-sOutputFile=-",
    options->file || "-",
    "-c",
    "quit",
  });

  Process.Process pid = Process.create_process( command, ([
    "stdin":fd,
    "stdout":fd4,
    "stderr":fd4,
  ]));
  fd->close();
  fd4->close();

  // Backend to use.
  Pike.Backend be = Pike.Backend();

  // Kill the gs binary after 30 seconds in case it hangs.
  mixed co =
    be->call_out(lambda(Process.Process pid) {
		   if (!pid->status()) {
		     pid->kill(9);
		   }
		 }, 30, pid);
  if(!options->file)
  {
    if(!has_value(data, "showpage"))
      data += "\nshowpage\n";
    be->add_file(fd2);
    Stdio.sendfile(({ data }), 0, 0, sizeof(data), 0, fd2,
		   lambda(int n) {
		     fd2->close();
		   });
  } else {
    fd2->close();
  }
  string output = "";
  be->add_file(fd3);
  fd3->set_nonblocking(lambda(mixed ignored, string s) {
			 output += s;
		       }, 0,
		       lambda() {
			 fd3->close();
			 destruct(fd3);
		       });
  mixed err = catch {
    while (fd3) {
      be(1.0);
    }
  };
#if 0
  if (err) {
    werror("Backend failed: %s\n", describe_backtrace(err));
  }
#endif
  be->remove_call_out(co);
  int ret_code = pid->wait();
  if(ret_code)
    error("Ghostscript failed with exit code: %O:\n%s\n", ret_code, output);
  object i= Image.PNM.decode( output );

  if (data) {

    if(data && sscanf(data, "%*s\n%%%%BoundingBox: %s\n", string bbox) == 2 &&
       !options->eps_crop)
    {
      int x0,x1,y0,y1;
      sscanf(bbox, "%d %d %d %d", x0,y0,x1,y1 );
      int llx = (int)((x0/72.0) * (options->dpi||100)+0.01);
      int lly = (int)((y0/72.0) * (options->dpi||100)+0.01);
      int urx = (int)((x1/72.0) * (options->dpi||100)+0.01);
      int ury = (int)((y1/72.0) * (options->dpi||100)+0.01);

      if(urx && ury)
	i = i->mirrory()->copy(llx,lly,urx,ury)->mirrory();
    }
  }
  return i;
}

//! Calls decode and returns the image in the "image"
//! index of the mapping. This method is present for
//! API reasons.
mapping _decode( string data, mapping|void options )
{
  return ([ "image":decode( data,options ) ]);
}



//! Encodes the image object @[img] as a postscript 3.0 file.
//! @param options
//!   Optional extra encoding parameters.
//!   @mapping
//!     @member int "dpi"
//!       The resolution of the encoded image. Defaults to 100.
//!     @member int(0..1) "eps"
//!       If the resulting image should be an eps instead of ps.
//!       Defaults to 0, no.
//!   @endmapping
string encode(  object img, mapping|void options )
{
  int w = img->xsize(), h = img->ysize();
  string i = (string)img;
  float scl = 72.0 / ((options&&options->dpi)||100);
  img = 0;
  string res;
  res =("%!PS-Adobe-3.0\n"
        "%%DocumentData: Clean8Bit\n"
	"%%BoundingBox: 0 0 "+(int)ceil(w*scl)+" "+(int)ceil(h*scl)+"\n"
	"%%EndComments\n"
	"%%BeginProlog\n"
	"30 dict begin\n"
	"/tmpstr 256 string def\n"
	"/dnl{currentfile tmpstr readline pop pop}bind def\n"
	"/di{dnl gsave scale 2 copy pop string/pxdat exch def\n"
	" 2 copy 8 [4 2 roll dup 0 0 4 2 roll neg 0 3 2 roll] {currentfile\n"
	" pxdat readstring pop}false 3 colorimage grestore}bind def\n"
	"%%EndProlog\n");

  res += sprintf("%d %d %f %f di\n", w, h, scl*w, scl*h);
  res +="%%BeginData "+sizeof(i)+" Binary Bytes\n" + i +"\n%%EndData\n";

  if( !options || !options->eps )
    res += "showpage\n";
  res += "%%Trailer\n" "end\n" "%%EOF\n";
  return res;
}

//! Same as encode. Present for API reasons.
function _encode = encode;

//! Decodes the header of the postscript @[data] into a mapping.
//!
//! @mapping
//!   @member int "xsize"
//!   @member int "ysize"
//!     Size of image
//!   @member string "type"
//!     File type information as MIME type. Always "application/postscript".
//!   @member string "color_space"
//!     Color space of image. "GRAYSCALE", "LAB", RGB", "CMYK" or "UNKNOWN"
//!  @endmapping
//!
public mapping decode_header(string data) {
  if(has_prefix(data, "\xc5\xd0\xd3\xc6")) {
    // DOS EPS Binary Header.
    int ps_start, ps_len, meta_start, meta_len, tiff_start, tiff_len, sum;
    sscanf(data, "%*4c%-4c%-4c%-4c%-4c%-4c%-4c%-2c",
	   ps_start, ps_len, meta_start, meta_len,
	   tiff_start, tiff_len, sum);
#if constant(Image.TIFF.decode_header)
    if (tiff_start && tiff_len) {
      return Image.TIFF.decode_header(data[tiff_start..tiff_start + tiff_len-1]);
    }
#endif
    data = data[ps_start..ps_start+ps_len-1];
  }

  if (has_prefix(data, "%!PS-Adobe-3.0 EPSF-3.0")) {
    int width, height, bits, ncols;
    int nbws, width2, encoding;
    string init_tag;
    if ((sscanf(data,
		  "%*s%%ImageData:%*[ ]%d%*[ ]%d%*[ ]%d%*[ ]%d%"
		  "*[ ]%d%*[ ]%d%*[ ]%d%*[ ]\"%s\"",
		  width, height, bits, ncols,
		  nbws, width2, encoding, init_tag) > 7) &&
	  (width == width2) && (width > 0) && (height > 0) && (bits == 8) &&
	  (< 1, 2 >)[encoding]) {
	    string color_space;
	    switch (ncols) {
	      case 1:
		color_space = "GRAYSCALE";
		break;
	      case 2:
		color_space = "LAB";
		break;
	      case 3:
		color_space = "RGB";
		break;
	      case 4:
		color_space = "CMYK";
	      break;
	      default:
		color_space = "UNKNOWN";
		break;
	    }
	return ([ "type": "application/postscript",
		  "xsize": width,
		  "ysize": height,
		  "color_space": color_space ]);
      }
    }
  return 0;
}