############################################################################
#
#	File:     psrecord.icn
#
#	Subject:  Procedures for PostScript record of window
#
#	Author:   Gregg M. Townsend
#
#	Date:     June 16, 2010
#
############################################################################
#
#   This file is in the public domain.
#
############################################################################
#
# 	Contributors: Stephen B. Wampler and Ralph E. Griswold
#
############################################################################
#
#	These procedures intercept graphics calls in order to produce
#	a PostScript copy of what is drawn.  The record is decidedly
#	imperfect.
#
############################################################################
#
#     These procedures produce a PostScript record of the screen display
#  of an Icon program.  The technique used is to intercept calls to
#  graphics functions and write PostScript before calling the built-in
#  versions.
#
#     Because the X emulation is imperfect, psrecord works best for
#  programs designed with it in mind.  Not all function calls are
#  intercepted; some such as CopyArea cannot be handled at all.  The
#  list of functions is in the internal routine PS_swap().  It is assumed
#  that there is only a single window and a single graphics context;
#  programs that switch among multiple graphics contexts will not be
#  recorded properly.
#
#     PostScript recording is enabled by calling PSEnable(window, filename)
#  any time after after the window has been opened.  (The procedures in
#  "autopost.icn" may be used for this.)  Defaults for PSEnable are
#  &window and "xlog.ps".  At the end, PSDone() should be called to
#  properly terminate the file; when PSDone() is not called, the file is
#  still be legal but lacks the "showpage" command needed for printing.
#
#     If the argument to PSDone is non-null, no showpage is written.
#  This is recommended for Encapsulated PostScript that is to be
#  placed in documents, since otherwise the bounding box resulting
#  from showpage may interfere with document layout.  showpage is, of
#  course, needed for PostScript that is to be printed stand-alone.
#
#     Additional procedures provide more detailed control but must be used
#  with care.  PSDisable() and PSEnable() turn recording off and back on;
#  any graphics state changes during this time (such as changing the
#  foreground color) are lost.  PSSnap() inserts a "copypage" command in
#  the output; this prints a snapshot of the partially constructed page
#  without erasing it.  PSRaw() writes a line of PostScript to the output
#  file.
#
#     PSStart(window, filename) is similar to PSEnable except that it
#  always starts a fresh output file each time it is called.
#
#     The output file is legal Encapsulated PostScript unless PSSnap is
#  used; PSSnap renders the output nonconforming because by definition
#  an EPS file consists of a single page and does not contain a "copypage"
#  command.  It should be possible to postprocess such output to make a
#  set of legal EPS files.
#
#     Some of the other limitations are as follows:
#         Only a few font names are recognized, and scaling is inexact.
#         Newlines in DrawString() calls are not interpreted.
#         Output via write() or writes() is not recorded.
#         The echoing of characters by read() or reads() is not recorded.
#         DrawCurve output is approximated by straight line segments.
#         Window resizing is ignored.
#         Drawing arguments must be explicit; few defaults are supplied.
#
############################################################################
#
#  Requires:  Version 9 graphics
#
############################################################################

global PS_active, PS_f, PS_win, PS_width, PS_height

#########################  External Functions  #########################

#  PSEnable(window, filename) -- enable PostScript recording.
#
#  window and filename are significant only on the very first call.

procedure PSEnable(w, f)		#: enable PostScript recording
   initial PS_init(w, f)
   if /PS_active := 1 then
      PS_swap()
   return
end

#  PSSnap() -- take snapshot at this point

procedure PSSnap()			#: take PostScript snapshot
   static inited
   if /PS_active then
      fail
   if /inited := 1 then {
      seek(PS_f, 1)
      write(PS_f, "%! nonconforming.......")	# overwrite 1st line
      seek(PS_f, 0)
   }
   PS_out("copypage")
   return
end

#  PSRaw(s) -- output a line of raw PostScript (at user's own risk)

procedure PSRaw(s)			#: output raw PostScript
   if /PS_active then
      fail
   return write(PS_f, s)
end

#  PSDisable() -- temporarily turn off recording

procedure PSDisable()			#: disable PostScript recording
   if \PS_active := &null then
      PS_swap()
   return
end

#  PSDone(sw) -- terminate output

procedure PSDone(sw)			#: terminate PostScript recording
   initial PS_init()
   PSDisable()
   if /sw then PS_out("showpage") 	# if sw nonnull, do not output
   PS_out("%%EOF")
   close(PS_f)
   return
end

#########################  Internal Functions  #########################

#  PS_swap() -- swap local functions for the real versions

procedure PS_swap()
   PS_attrib :=: WAttrib
   PS_bg :=: Bg
   PS_clip :=: Clip
   PS_drawarc :=: DrawArc
   PS_drawcircle :=: DrawCircle
   PS_drawcurve :=: DrawCurve
   PS_drawline :=: DrawLine
   PS_drawrect :=: DrawRectangle
   PS_drawpoint :=: DrawPoint
   PS_drawsegment :=: DrawSegment
   PS_drawstring :=: DrawString
   PS_erasearea :=: EraseArea
   PS_fg :=: Fg
   PS_fillarc :=: FillArc
   PS_fillcircle :=: FillCircle
   PS_fillrect :=: FillRectangle
   PS_fillpoly :=: FillPolygon
   PS_flush :=: WFlush
   PS_font :=: Font
   return
end

#  PS_init(w, f) -- initialize recording system

procedure PS_init(a[])
   if /PS_active then PSStart ! a
   return
end

procedure PSStart(a[])
local fname, scale, psw, psh, llx, lly

   if \PS_active then PSDone()
   PS_afix(a)
   PS_win := \a[1] | \&window | runerr(140, a[1])
   fname := \a[2] | "xlog.ps"
   PS_f := open(fname, "w") | stop("can't open", fname)

   # calculate output scaling
   # max (&default) scaling is 1.0 (72 pixels per inch)
   # max size image allowed comes within 0.5" of all four borders
   PS_width := WAttrib(PS_win, "width")
   PS_height := WAttrib(PS_win, "height")
   scale := 1.0
   scale >:= 72 * (8.5 - 0.5 - 0.5) / PS_width
   scale >:= 72 * (11.0 - 0.5 - 0.5) / PS_height

   # position window in center of page
   psw := integer(scale * PS_width + 0.9999)	# width in ps coords
   psh := integer(scale * PS_height + 0.9999)	# height
   llx := integer((72 * 8.5 - psw) / 2)		# center horizontally
   lly := integer((72 * 11.0 - psh) / 2)	# center vertically
   if lly + psh < 72 * 9.5 then
      lly := integer(72 * 9.5 - psh)		# but not over 1.5" from top

   # write EPS header
   PS_out("%!PS-Adobe-3.0 EPSF-3.0")
   PS_out("%%BoundingBox:", llx, lly, llx + psw + 1, lly + psh + 1)
   PS_out("%%Creator:", &progname)
   PS_out("%%CreationDate:", &dateline)
   PS_out("%%EndComments")
   PS_out()

   every PS_out(![			# output PostScript file header

      # define variables now so that bound procs get correct versions
      "/BGR 0 def /BGG 0 def /BGB 0 def",

      # shorthand procedures
      "/bd {bind def} bind def",
      "/m {moveto} bd",
      "/l {lineto} bd",
      "/s {stroke} bd",
      "/f {fill} bd",

      # construct a rectangular path; usage is: w h x y <r>
"/r {moveto dup 0 exch rlineto exch 0 rlineto neg 0 exch rlineto closepath} bd",

      # procedures for remembering state
      "/fg {setrgbcolor} bd",					# foreground
      "/bg {/BGB exch def /BGG exch def /BGR exch def} bd",	# background
      "/ft {findfont exch dup neg matrix scale makefont setfont} bd", # font

      # A new clip path may not be inside old path as needed by PS.
      # Save the old context, pop back to full-screen graphics state,
      # restore other context, and set clip path.
      "/cp {currentfont currentrgbcolor grestore gsave setrgbcolor setfont",
      "     r clip newpath } bd",

      # drawing procedures
      "/t {moveto show newpath} bd",				# text string
      "/p {0.5 0 360 arc fill} bd",				# point
      "/g {moveto lineto stroke} bd",				# line segment
      "/pf {closepath fill} bd",				# filled polygon
      "/c {0 360 arc stroke} bd",				# circle
      "/cf {0 360 arc fill} bd",				# filled circle
      "/a {gsave translate 1.0 exch scale arc stroke grestore} bd",	# arc
      "/af {gsave translate 1.0 exch scale 0 0 moveto arc fill grestore} bd",
      "/er {gsave r BGR BGG BGB setrgbcolor fill grestore} bd",	# erase area
   ])

   # establish coordinate system
   PS_out(llx, lly + psh, "translate")
   PS_out(psw + 1, -(psh + 1), "0 0 r clip newpath")
   PS_out(scale, -scale, "scale")
   PS_out("0.5 0.5 translate")
   PS_out("gsave")				# save full-window gpx env

   # swap our routines for those of Icon
   if /PS_active := 1 then
      PS_swap()

   # note graphics values in PS file
   Font(PS_win, Font(PS_win))
   Fg(PS_win, Fg(PS_win))
   Bg(PS_win, Bg(PS_win))

   PS_out(PS_width, PS_height, "0 0 er")	# fill background
   write(PS_f)
return
end

#  PS_out(s, s, ...) -- output strings to PS file, with spaces between

procedure PS_out(a[])
   if /a[1] then
      return
   writes(PS_f, get(a))
   while writes(PS_f, " ", get(a))
   write(PS_f)
   return
end

#  PS_path(a, s) -- output path from a[2..*] followed by command s

procedure PS_path(a, s)
   local i

   PS_out(a[2], a[3], "m")
   every i := 4 to *a - 3 by 2 do
      PS_out(a[i], a[i+1], "l")
   PS_out(a[-2], a[-1], "l", s)
   return
end

#  PS_afix(a) -- fix arg list to ensure that first arg is a window

procedure PS_afix(a)
   if not (type(a[1]) == "window") then
      push(a, &window)
   return a
end

####################  Icon Function Substitutes  ####################

procedure PS_flush(a[])		# replaces WFlush
   # we don't know why they're flushing, but we'll flush, too
   flush(PS_f)
   return PS_flush ! a
end


procedure PS_bg(a[])		# replaces Bg
   PS_afix(a)
   # note that following line fails if there is no a[2]
   PS_out(PS_color(a[2]), "bg")
   return PS_bg ! a
end

procedure PS_fg(a[])		# replaces Fg
   PS_afix(a)
   # note that following line fails if there is no a[2]
   PS_out(PS_color(a[2]), "fg")
   return PS_fg ! a
end

procedure PS_color(color)	# parse color, return string of PS r, g, b
   local r, g, b
   (ColorValue(PS_win, color) | fail) ? {
      r := tab(many(&digits));  move(1)
      g := tab(many(&digits));  move(1)
      b := tab(many(&digits))
      }
   return (r / 65535.0) || " " || (g / 65535.0) || " " || (b / 65535.0)
end

procedure PS_drawpoint(a[])	# replaces DrawPoint
   local i

   PS_afix(a)
   every i := 2 to *a by 2 do
      PS_out(a[i], a[i+1], "p")
   return PS_drawpoint ! a
end

procedure PS_drawsegment(a[])	# replaces DrawSegment
   local i

   PS_afix(a)
   every i := 2 to *a by 4 do
      PS_out(a[i], a[i+1], a[i+2], a[i+3], "g")
   return PS_drawsegment ! a
end

procedure PS_drawline(a[])	# replaces DrawLine
   local i

   PS_afix(a)
   if *a == 5 then
      PS_out(a[2], a[3], a[4], a[5], "g")
   else
      PS_path(a, "s")
   return PS_drawline ! a
end

procedure PS_drawcurve(a[])	# replaces DrawCurve -- approx with line segs
   local i

   PS_afix(a)
   PS_path(a, "s")
   return PS_drawcurve ! a
end

procedure PS_drawrect(a[])	# replaces DrawRectangle
   local i

   PS_afix(a)
   every i := 2 to *a by 4 do
      PS_out(a[i+2], a[i+3], a[i], a[i+1], "r s")
   return PS_drawrect ! a
end

procedure PS_fillrect(a[])	# replaces FillRectangle
   local i

   PS_afix(a)
   every i := 2 to *a by 4 do
      PS_out(a[i+2], a[i+3], a[i], a[i+1], "r f")
   return PS_fillrect ! a
end

procedure PS_fillpoly(a[])	# replaces FillPolygon
   local i

   PS_afix(a)
   PS_path(a, "pf")
   return PS_fillpoly ! a
end


procedure PS_clip(a[])		# replaces Clip
   PS_area(a, "cp")
   return PS_clip ! a
end

procedure PS_erasearea(a[])	# replaces EraseArea
   PS_area(a, "er")
   return PS_erasearea ! a
end

procedure PS_area(a, cmd)	# generate w, h, x, y, and cmd, with defaults
   local x, y, w, h
   PS_afix(a)

   x := \a[2] | 0
   y := \a[3] | 0
   w := (0 ~= \a[4]) | PS_width
   h := (0 ~= \a[5]) | PS_height
   PS_out(w, h, x, y, cmd)
end

procedure PS_drawcircle(a[])	# replaces DrawCircle
   PS_arc(a, 0, "")
   return PS_drawcircle ! a
end

procedure PS_fillcircle(a[])	# replaces FillCircle
   PS_arc(a, 0, "f")
   return PS_fillcircle ! a
end
  
procedure PS_drawarc(a[])	# replaces DrawArc
   PS_arc(a, 1, "")
   return PS_drawarc ! a
end

procedure PS_fillarc(a[])	# replaces FillArc
   PS_arc(a, 1, "f")
   return PS_fillarc ! a
end

procedure PS_arc(a, n, f)	# handle draw/fill arc/circle, append f to cmd
   local x, y, w, h, ar, a1, a2, r, i
   static mul
   initial mul := 180 / &pi

   PS_afix(a)
   every i := 2 to *a by (5 + n) do {
      x := a[i]
      y := a[i+1]
      w := a[i+2] 
      h := a[i+2+n]
      a1 := (\a[i+n+3] * mul) | 0.0
      a2 := (\a[i+n+4] * mul) | 360.0
      if n = 1 then {		# if Draw/Fill Arc
         r := w / 2.0			# radius
         x +:= r			# center coordinates
         y +:= h / 2.0
         }
      else			# Draw/Fill Circle
         r := w
      if w = h & abs(a2) > 359.99 then	# if circle
         PS_out(x, y, r, "c" || f)
      else {				# general case
         if a2 < 0 then {
            a1 := a1 + a2	# ensure counterclockwise arc (in PS coords)
            a2 := -a2
            }
         if w = 0 then
            ar := 0.0
         else
            ar := real(h) / real(w)
         PS_out("0 0", r, a1, a1 + a2, ar, x, y, "a" || f)
         }
      }
   return
end

procedure PS_font(a[])	     	# replaces Font (very crudely)
   local ret, xname, psname, n

   PS_afix(a)
   if not (ret := PS_font ! a) then
      fail
   if xname := \a[2] then {
      map(xname) ? {
         if tab(many(&digits)) & ="x" & tab(many(&digits)) & pos(0) then
            psname := "/Courier"
         else if find("fixed" | "courier" | "typewriter") & find("bold") then
            psname := "/Courier-Bold"
         else if find("fixed" | "courier" | "typewriter") then
            psname := "/Courier"
         else if find("helvetica" | "sans") & find("bold") then
            psname := "/Helvetica-Bold"
         else if find("helvetica" | "sans") & find("oblique") then
            psname := "/Helvetica-Oblique"
         else if find("helvetica" | "sans") then
            psname := "/Helvetica"
         else if find("times") & find("bold")then
            psname := "/Times-Bold"
         else if find("times") & find("italic")then
            psname := "/Times-Italic"
         else if find("times") then
            psname := "/Times-Roman"
         else if find("bold") then
            psname := "/Palatino-Bold"
         else if find("italic") then
            psname := "/Palatino-Italic"
         else
            psname := "/Palatino-Roman"
         }
      n := WAttrib(PS_win, "ascent") + 1	# could possibly be smarter
      PS_out(n, psname, "ft  %", xname)
      }
   return ret
end

procedure PS_drawstring(a[])		# replaces DrawString
   PS_afix(a)
   PS_psstring(a[4])
   PS_out("", a[2], a[3], "t")
   return PS_drawstring ! a
end

procedure PS_psstring(s)		# output a string as a PS string
   s ? {
      writes(PS_f, "(")
      while writes(PS_f, tab(upto('()\\'))) do
         writes(PS_f, "\\", move(1))
      writes(PS_f, tab(0), ")")
   }
   return
end

#  PS_attrib() -- handle WAttrib calls
#
#  Any attribute that is accepted here should also be checked and set to
#  the correct value during initialization in order to catch attributes
#  that were set on the open() call.

procedure PS_attrib(alist[])		# replaces WAttrib
   local win, ret, name, val, a

   PS_afix(alist)
   ret := alist
   ret := PS_attrib ! alist
   win := pop(alist)			# remove window arg
   every a := !alist do a ? {		# process each attribute
      name := tab(upto('=')) | next
      move(1)
      val := tab(0)
      case name of {
         "fg":    Fg(win, val)
         "bg":    Bg(win, val)
         "font":  Font(win, val)
         }
      }
   return a ~=== ret			# return value or fail if WAttrib failed
end
