#!/usr/bin/python

#################################################################
## This script copies a postscript file from input to output, 
## adding a rubber-stamp-like overlay showing the text specified 
## in the command-line arguments.
##
## Copyright (c) 2006 David LaRose, dlr@cs.cmu.edu
## All rights reserved.
##
## Permission is granted to use this software for any ethical
## purpose.
##
## THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
## CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES.
#################################################################

import string
import sys
import optparse

## Define a static strings for usage, etc.
s_version = '%prog 1.1'
s_usage = """%prog [options] <stampText> <inputFile> <outputFile>

Copies a postscript file from input to output, adding a
rubber-stamp-like overlay showing the specified text.

If outputFile is omitted, or is set to the single character '-', then
output will be written to stdout.

If both outputFile and inputFile are omitted, or if inputFile is set
to the single character '-', then input will be read from stdin.

Use the option --help to see a list of available options."""


## Define static strings containing postscript code to draw the
## rubber stamp image
s_introComment = "% ========= Beginning of code inserted by psStamp ========="


s_outroComment = "% ========= End of code inserted by psStamp ========="


s_prologString = """% operator dlr_textBoundingBox: string dlr_textBoundingBox llx lly urx ury
% Returns the lower-left and upper-right corners of the user-coordinate
% bounding box of the stacked string, assuming it will be rendered with
% the current font settings.
/dlr_textBoundingBox {
    gsave		% Save graphics state.
        % Align with device coordinates so that pathbbox will work as expected.
        [1 0 0 1 0 0] setmatrix 

        % Pretend we're going to draw the stacked text.
        newpath
        0 0 moveto	% Start drawing at origin.

        % Instead of drawing, just calculate its bounding box.
        true charpath		% Convert string on stack to path.
        flattenpath pathbbox	% Get bounding box for the stacked path.
    grestore
} def

% operator dlr_stampOutline: string dlr_stampOutline -

% Draws an approximately rectangular stroke arouund the area that the
% stacked string will render to.  Many rubber stamps have a border
% which surrounds the entire stamp, and this routine simulates that
% border.
/dlr_stampOutline {
    % Make a dict for local variables, and push it on the dictionary stack.
    6 dict begin
    
        % Get the bounding box of the text in user coords.
        dlr_textBoundingBox

        % Capture the results of dlr_textBoundingBox into local variables.
        /bbUpperRightY exch def
        /bbUpperRightX exch def
        /bbLowerLeftY exch def
        /bbLowerLeftX exch def

        % The bounding box is right on the edges of the letters.
        % Calculate 10% of the height of the bounding box, so that we
        % can draw a box that's comfortably larger than the printed string.
        /bboxHeight bbUpperRightY bbLowerLeftY sub def
        /bufferSize bboxHeight 0.20 mul def

        % Modify the bounding box corners.
        /upperRightY bbUpperRightY bufferSize add def
        /upperRightX bbUpperRightX bufferSize add def
        /lowerLeftY bbLowerLeftY bufferSize sub def
        /lowerLeftX bbLowerLeftX bufferSize sub def
        
        % % Draw bounding box.
        % newpath
        % lowerLeftX lowerLeftY moveto	
        % upperRightX lowerLeftY lineto
        % upperRightX upperRightY lineto
        % lowerLeftX upperRightY lineto
        % % lowerLeftX lowerLeftY lineto
        % closepath
        % stroke
        
        % Draw bounding box, with nice rounded corners.
        newpath
        lowerLeftX bbLowerLeftY moveto
        lowerLeftX lowerLeftY lowerLeftX lowerLeftY
            bbLowerLeftX lowerLeftY curveto
        bbUpperRightX lowerLeftY lineto
        upperRightX lowerLeftY upperRightX lowerLeftY
            upperRightX bbLowerLeftY curveto
        upperRightX bbUpperRightY lineto
        upperRightX upperRightY upperRightX upperRightY
            bbUpperRightX upperRightY curveto
        bbLowerLeftX upperRightY lineto
        lowerLeftX upperRightY lowerLeftX upperRightY
            lowerLeftX bbUpperRightY curveto
        closepath
        stroke

    % Pop our local dict off the stack
    end
} def

% operator dlr_stamp: string angle xcoord ycoord stamp -
% Draws the string in a way that looks like a rubber stamp
% at xcoord, ycoord, with the image rotated by angle degrees.
/dlr_stamp {
    gsave		% Save graphics state.
        % Move to the position specified on stack.
        translate

        % Rotate by the angle specified on stack.
        rotate

        % Copy the input string, since we'll use it twice below.
        dup		

        dlr_stampOutline
        
        % Now draw an outline of the text
        newpath
        0 0 moveto	% Start drawing at origin.
        true charpath	% Convert string on stack to path.
        stroke		% And stroke it.

    grestore
} def
"""

s_showpageString0 = """
/dlr_savedShowpage /showpage load def
/dlr_StampDelivered 0 def
/showpage {
    dlr_StampDelivered 0 eq {
        /dlr_StampDelivered 1 def
        gsave
            initgraphics
"""            


s_stampLinesComment = "            % dlr_stamp commands go here"


s_showpageString1 = """        grestore
    } if
    dlr_savedShowpage
} def
"""


## Here's a convenience function to help with copying lines from input
## to output.
def copyUntil(inputFile, outputFile, markerString):
  while 1:
    inputLine = inputFile.readline()
    if inputLine == '':
      return False
    # end if
    outputFile.write(inputLine)
    if (string.find(inputLine, markerString) != -1):
      return True
    # end if
  # end while
# end def


## Now the main program.
if __name__ == '__main__':

  ## Define command line options.
  argParser = optparse.OptionParser(version=s_version, usage=s_usage)
  argParser.add_option(
    '-c', '--color', action='store', type='string',
    dest='colorString', default='0.65/0.1/0.25',
    metavar='<colorString>',
    help=('RGB specification for stamp ink color.  This must have the form '
          + '<Num>/<Num>/<Num>, where each number is in the range '
          + '[0.0 - 1.0]'))
  argParser.add_option(
    '-f', '--font', action='store', type='string',
    # dest='font', default='BitstreamVeraSans-Bold-ISOLatin1',
    dest='font', default='Sans-Bold',
    metavar='<fontName>',
    help=('Font in which to render text.  Note that this must be a valid '
          + 'postscript font.  We do not check the validity of this argument'))
  argParser.add_option(
    '-r', '--rotation', action='store', type='float',
    dest='rotation', default=-7.5, metavar='[Num]',
    help='Rotation angle of stamp in degrees counterclockwise')
  argParser.add_option(
    '-s', '--fontsize', action='store', type='int',
    dest='fontsize', default=35, metavar='[Num]',
    help='Font size in points')
  argParser.add_option(
    '-w', '--linewidth', action='store', type='float',
    dest='linewidth', default=1.5, metavar='[Num]',
    help='line width to use for rendering')
  argParser.add_option(
    '-x', '--xposition', action='store', type='float',
    dest='xPosition', default=2.5, metavar='[Num]',
    help='X position of stamp in inches from bottom left corner')
  argParser.add_option(
    '-y', '--yposition', action='store', type='float',
    dest='yPosition', default=1.5, metavar='[Num]',
    help='Y position of stamp in inches from bottom left corner')
  
  ## Parse command line.
  (options, args) = argParser.parse_args()
  if len(args) != 3:
    print argParser.get_usage()
    sys.exit(65)
  # end if
  stampText = args[0]
  inputFileName = args[1]
  outputFileName = args[2]

  ## Sort out the RGB color
  try:
    rgbColor = map(float, string.split(options.colorString, '/'))
    if len(rgbColor) != 3:
      raise ValueError
    # end if
  except:
    sys.stderr.write('Error: Invalid color string: '
                     + options.colorString + '\n')
    sys.exit(65)
  # end try
  
  ## Open files if necessary.
  if inputFileName == '-':
    inputFile = sys.stdin
  else:
    inputFile = open(inputFileName, 'r')
  # end if

  if outputFileName == '-':
    outputFile = sys.stdout
  else:
    outputFile = open(outputFileName, 'w')
  # end if


  ## Format up the PS commands to print the stamp.  Note that PS points
  ## are 72 per inch.
  fontAndColorCommand = (
    """            %% Set up font, line color, and line width.
            /%s findfont
            %d scalefont
            setfont	       
            %f %f %f setrgbcolor
            %f setlinewidth"""
    % (options.font, options.fontsize, rgbColor[0], rgbColor[1], rgbColor[2],
       options.linewidth))
  stampCommand = (
    """            (%s) %f %f %f dlr_stamp"""
    % (stampText, options.rotation,
       72 * options.xPosition, 72 * options.yPosition))


  ## The file parsing is really like a little state machine, but it's
  ## simple enough that we just implement the states explicitly here.

  ## State 1: Waiting for prolog
  copyUntil(inputFile, outputFile, '%%BeginProlog')

  ## States 2 & 3: Checking to see if our function defs have already
  ## been inserted in this file, and inserting them if not.  If
  ## they're already there, we simply add our new commands in the
  ## middle of the existing code.
  inputLine = inputFile.readline()
  if string.find(inputLine, s_introComment) == -1:
    outputFile.write(s_introComment + '\n\n')
    outputFile.write(s_prologString)
    outputFile.write(s_showpageString0)
    outputFile.write(s_stampLinesComment + '\n')
    outputFile.write(fontAndColorCommand + '\n')
    outputFile.write(stampCommand + '\n')
    outputFile.write(s_showpageString1)
    outputFile.write('\n' + s_outroComment + '\n\n')
  else:
    outputFile.write(inputLine)
    copyUntil(inputFile, outputFile, s_stampLinesComment)
    outputFile.write(fontAndColorCommand + '\n')
    outputFile.write(stampCommand + '\n')
    copyUntil(inputFile, outputFile, s_outroComment)
  # end if


  ## State 4: Copy the rest of the file.
  outputFile.write(inputFile.read())

  ## Clean up.
  outputFile.close()
  inputFile.close()
  sys.exit(0)
# end if
