/*----------------------------------------------------------------------------
                                 pstopnm
------------------------------------------------------------------------------
  Use Ghostscript to convert a Postscript file into a PBM, PGM, or PNM
  file.

-----------------------------------------------------------------------------*/
#define _BSD_SOURCE 1   /* Make sure strdup() is in string.h */
#define _POSIX_SOURCE 1     /* Make sure popen(), pclose() are in stdio.h */
#define _POSIX_C_SOURCE 2   /* Same as above */

#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <sys/wait.h>   /* For popen() exit codes */
#include <sys/stat.h>
#include "pnm.h"

enum orientation {PORTRAIT, LANDSCAPE, UNSPECIFIED};
struct box {
    /* Description of a rectangle within an image; all coordinates 
       measured in pixels with lower left corner of page being the 
       origin.
       */
    int llx;  /* lower left X coord */
        /* -1 for llx means whole box is undefined. */
    int lly;  /* lower left Y coord */
    int urx;  /* upper right X coord */
    int ury;  /* upper right Y coord */
};

struct cmdline_info {
    /* All the information the user supplied in the command line,
       in a form easy for the program to use.
    */
    char *input_filespec;  /* Filespecs of input files */
    int forceplain;
    struct box extract_box;
    int nocrop;
    int format_type;
    int verbose;
    float xborder;
    int xmax;
    int xsize;
    float yborder;
    int ymax;
    int ysize;
    enum orientation orientation;
    int goto_stdout;
} cmdline;


static void
parse_command_line(int argc, char ** argv,
                   struct cmdline_info *cmdline_p) {
/*----------------------------------------------------------------------------
   Note that the file spec array we return is stored in the storage that
   was passed to us as the argv array.
-----------------------------------------------------------------------------*/
    optStruct *option_def = malloc(100*sizeof(optStruct));
        /* Instructions to OptParseOptions2 on how to parse our options.
         */
    optStruct2 opt;

    unsigned int option_def_index;

    int pbm_opt, pgm_opt, ppm_opt;
    int portrait_opt, landscape_opt;
    float llx, lly, urx, ury;
    
    option_def_index = 0;   /* incremented by OPTENTRY */
    OPTENTRY(0,   "forceplain", OPT_FLAG,   &cmdline_p->forceplain,     0);
    OPTENTRY(0,   "llx",        OPT_FLOAT,  &llx,                       0);
    OPTENTRY(0,   "lly",        OPT_FLOAT,  &lly,                       0);
    OPTENTRY(0,   "urx",        OPT_FLOAT,  &urx,                       0);
    OPTENTRY(0,   "ury",        OPT_FLOAT,  &ury,                       0);
    OPTENTRY(0,   "nocrop",     OPT_FLAG,   &cmdline_p->nocrop,         0);
    OPTENTRY(0,   "pbm",        OPT_FLAG,   &pbm_opt,                   0);
    OPTENTRY(0,   "pgm",        OPT_FLAG,   &pgm_opt,                   0);
    OPTENTRY(0,   "ppm",        OPT_FLAG,   &ppm_opt,                   0);
    OPTENTRY(0,   "verbose",    OPT_FLAG,   &cmdline_p->verbose,        0);
    OPTENTRY(0,   "xborder",    OPT_FLOAT,  &cmdline_p->xborder,        0);
    OPTENTRY(0,   "xmax",       OPT_UINT,   &cmdline_p->xmax,           0);
    OPTENTRY(0,   "xsize",      OPT_UINT,   &cmdline_p->xsize,          0);
    OPTENTRY(0,   "yborder",    OPT_FLOAT,  &cmdline_p->yborder,        0);
    OPTENTRY(0,   "ymax",       OPT_UINT,   &cmdline_p->ymax,           0);
    OPTENTRY(0,   "ysize",      OPT_UINT,   &cmdline_p->ysize,          0);
    OPTENTRY(0,   "portrait",   OPT_FLAG,   &portrait_opt,              0);
    OPTENTRY(0,   "landscape",  OPT_FLAG,   &landscape_opt,             0);
    OPTENTRY(0,   "stdout",     OPT_FLAG,   &cmdline_p->goto_stdout,    0);

    /* Set the defaults */
    cmdline_p->forceplain = FALSE;
    cmdline_p->nocrop = FALSE;
    cmdline_p->verbose = FALSE;
    pbm_opt = pgm_opt = ppm_opt = FALSE;
    portrait_opt = landscape_opt = FALSE;
    llx = lly = urx = ury = -1;
    cmdline_p->xmax = 612;
    cmdline_p->ymax = 792;
    cmdline_p->xsize = cmdline_p->ysize = -1;
    cmdline_p->xborder = cmdline_p->yborder = 0.1;

    opt.opt_table = option_def;
    opt.short_allowed = FALSE;  /* We have no short (old-fashioned) options */
    opt.allowNegNum = FALSE;  /* We have no parms that are negative numbers */

    pm_optParseOptions2(&argc, argv, opt, 0);
        /* Uses and sets argc, argv, and some of *cmdline_p and others. */

    if (argc-1 == 0)
        cmdline_p->input_filespec = "-";  /* stdin */
    else if (argc-1 == 1)
        cmdline_p->input_filespec = argv[1];
    else 
        pm_error("Too many arguments (%d).  "
                 "Only need one: the Postscript filespec", argc-1);

    if (portrait_opt & !landscape_opt)
        cmdline_p->orientation = PORTRAIT;
    else if (!portrait_opt & landscape_opt)
        cmdline_p->orientation = LANDSCAPE;
    else if (!portrait_opt & !landscape_opt)
        cmdline_p->orientation = UNSPECIFIED;
    else
        pm_error("Cannot specify both -portrait and -landscape options");

    if (pbm_opt)
        cmdline_p->format_type = PBM_TYPE;
    else if (pgm_opt)
        cmdline_p->format_type = PGM_TYPE;
    else if (ppm_opt)
        cmdline_p->format_type = PPM_TYPE;
    else
        cmdline_p->format_type = PPM_TYPE;

    /* If any one of the 4 bounding box coordinates is given on the
       command line, we default any of the 4 that aren't.  
    */
    if (llx != -1 || lly != -1 || urx != -1 || ury != -1) {
        if (llx == -1) cmdline_p->extract_box.llx = 72;
        else cmdline_p->extract_box.llx = llx * 72;
        if (lly == -1) cmdline_p->extract_box.lly = 72;
        else cmdline_p->extract_box.lly = lly * 72;
        if (urx == -1) cmdline_p->extract_box.urx = 540;
        else cmdline_p->extract_box.urx = urx * 72;
        if (ury == -1) cmdline_p->extract_box.ury = 720;
        else cmdline_p->extract_box.ury = ury * 72;
    } else {
        cmdline_p->extract_box.llx = -1;
    }

}



static void
add_ps_to_filespec(const char orig_filespec[], char ** const new_filespec_p,
                   const int verbose) {
/*----------------------------------------------------------------------------
   If orig_filespec[] names an existing file, but the same name with ".ps"
   added to the end does, return the name with the .ps attached.  Otherwise,
   just return orig_filespec[].

   Return the name in newly malloc'ed storage, pointed to by
   *new_filespec_p.
-----------------------------------------------------------------------------*/
    struct stat statbuf;
    int stat_rc;

    stat_rc = lstat(orig_filespec, &statbuf);
    
    if (stat_rc == 0)
        *new_filespec_p = strdup(orig_filespec);
    else {
        char *filespec_plus_ps;

        filespec_plus_ps = malloc(strlen(orig_filespec) + 4);
        strcpy(filespec_plus_ps, orig_filespec);
        strcat(filespec_plus_ps, ".ps");

        stat_rc = lstat(filespec_plus_ps, &statbuf);
        if (stat_rc == 0)
            *new_filespec_p = strdup(filespec_plus_ps);
        else
            *new_filespec_p = strdup(orig_filespec);
    }
    if (verbose)
        pm_message("Input file is %s", *new_filespec_p);
}



static void
compute_size_res(const struct cmdline_info cmdline, 
                 const enum orientation orientation, 
                 const struct box bordered_box,
                 int * const xsize_p, int * const ysize_p,
                 int * const xres_p, int * const yres_p) {

    int sx, sy;

    if (orientation == LANDSCAPE) {
        sx = bordered_box.ury - bordered_box.lly;
        sy = bordered_box.urx - bordered_box.llx;
    } else {
        sx = bordered_box.urx - bordered_box.llx;
        sy = bordered_box.ury - bordered_box.lly;
    }
    if (cmdline.ysize == -1 && cmdline.xsize == -1) {
        /* User didn't specify either x or y size, so we compute them
           from xmax and ymax
           */
        *yres_p = cmdline.ymax * 72 / sy;
        *xres_p = *yres_p = 
            min(cmdline.xmax * 72 / sx, cmdline.ymax * 72 / sy);
        
        if (cmdline.nocrop) {
            *xsize_p = cmdline.xmax;
            *ysize_p = cmdline.ymax;
        } else {
            *xsize_p = (int) (sx * *xres_p / 72 + 0.5);
            *ysize_p = (int) (sy * *yres_p / 72 + 0.5);
        }
    } else {
        /* Compute the x and y size from the specified x and/or y sizes */
        if (cmdline.xsize != -1) {
            *xsize_p = cmdline.xsize;
            *xres_p = cmdline.xsize * 72 / sx;
            if (cmdline.ysize == -1) {
                *yres_p = *xres_p;
                *ysize_p = (int) (sy * *yres_p/72 + 0.5);
            }
        }

        if (cmdline.ysize != -1) {
            *ysize_p = cmdline.ysize;
            *yres_p = cmdline.ysize * 72 / sy;
            if (cmdline.xsize == -1) {
                *xres_p = *yres_p;
                *xsize_p = (int) (sx * *xres_p/72 + 0.5);
            }
        }
    }
}



static struct box
compute_box_to_extract(const struct box cmdline_extract_box,
                       const char input_filespec[],
                       const int verbose) {

    struct box retval;

    if (cmdline.extract_box.llx != -1)
        /* User told us what box to extract, so that's what we'll do */
        retval = cmdline_extract_box;
    else {
        /* Try to get the bounding box from the Postscript %%BoundingBox
           statement in the input.
           */
        struct box ps_bb;  /* Box described by %%BoundingBox stmt in input */

        if (strcmp(input_filespec, "-") == 0)
            /* Can't read stdin, because we need it positioned for the 
               Ghostscript interpreter to read it.
            */
            ps_bb.llx = -1;
        else {
            FILE *infile;
            int found_BB, eof;  /* logical */
            infile = pm_openr((char*)input_filespec);
            
            found_BB = FALSE;
            eof = FALSE;
            while (!eof && !found_BB) {
                char line[200];
                
                if (fgets(line, sizeof(line), infile) == NULL)
                    eof = TRUE;
                else {
                    int rc;
                    rc = sscanf(line, "%%%%BoundingBox: %d %d %d %d",
                                &ps_bb.llx, &ps_bb.lly, 
                                &ps_bb.urx, &ps_bb.ury);
                    if (rc == 4) 
                        found_BB = TRUE;
                }
            }
            fclose(infile);

            if (!found_BB) {
                ps_bb.llx = -1;
                pm_message("Warning: no %%%%BoundingBox statement "
                           "in the input or command line.\n"
                           "Will use defaults");
            }
        }
        if (ps_bb.llx != -1) {
            if (verbose)
                pm_message("Using %%%%BoundingBox statement from input.");
            retval = ps_bb;
        } else { 
            /* Use the center of an 8.5" x 11" page with 1" border all around*/
            retval.llx = 72;
            retval.lly = 72;
            retval.urx = 540;
            retval.ury = 720;
        }
    }
    if (verbose)
        pm_message("Extracting the box ((%d,%d),(%d,%d))",
                   retval.llx, retval.lly, retval.urx, retval.ury);
    return retval;
}



static enum orientation
compute_orientation(const struct cmdline_info cmdline, 
                    const struct box extract_box) {

    enum orientation retval;
    const int input_width = extract_box.urx - extract_box.llx;
    const int input_height = extract_box.ury - extract_box.lly;

    if (cmdline.orientation != UNSPECIFIED)
        retval = cmdline.orientation;
    else {
        if ((cmdline.xsize == -1 || cmdline.ysize == -1) &
            (cmdline.xsize != -1 || cmdline.ysize != -1)) {
            /* User specified one output dimension, but not the other,
               so we can't use output dimensions to make the decision.  So
               just use the input dimensions.
               */
            if (input_height > input_width) retval = PORTRAIT;
            else retval = LANDSCAPE;
        } else {
            int output_width, output_height;
            if (cmdline.xsize != -1) {
                /* He gave xsize and ysize, so that's the output size */
                output_width = cmdline.xsize;
                output_height = cmdline.ysize;
            } else {
                /* Well then we'll just use his (or default) xmax, ymax */
                output_width = cmdline.xmax;
                output_height = cmdline.ymax;
            }

            if (input_height > input_width && output_height > output_width)
                retval = PORTRAIT;
            else 
                if (input_height < input_width && output_height < output_width)
                    retval = PORTRAIT;
            else 
                retval = LANDSCAPE;
        }
    }
    return retval;
}



static struct box
add_borders(const struct box input_box, 
            const float xborder_scale, float yborder_scale,
            const int verbose) {

    struct box retval;

    const int left_right_border_size = 
        (int) ((input_box.urx - input_box.llx) * xborder_scale + 0.5);
    const int top_bottom_border_size = 
        (int) ((input_box.ury - input_box.lly) * yborder_scale + 0.5);

    retval.llx = input_box.llx - left_right_border_size;
    retval.lly = input_box.lly - top_bottom_border_size;
    retval.urx = input_box.urx + left_right_border_size;
    retval.ury = input_box.ury + top_bottom_border_size;

    if (verbose)
        pm_message("With borders, extracted box is ((%d,%d),(%d,%d))",
                   retval.llx, retval.lly, retval.urx, retval.ury);

    return retval;
}



static char *
compute_pstrans(const struct box box, const enum orientation orientation,
                const int xsize, const int ysize, 
                const int xres, const int yres) {

    static char retval[100]; 

    if (orientation == PORTRAIT) {
        int llx, lly;
        llx = box.llx - (xsize * 72 / xres - (box.urx - box.llx)) / 2;
        lly = box.lly - (ysize * 72 / yres - (box.ury - box.lly)) / 2;
        sprintf(retval, "%d neg %d neg translate", llx, lly);
    } else {
        int llx, ury;
        llx = box.llx - (ysize * 72 / yres - (box.urx - box.llx)) / 2;
        ury = box.ury + (xsize * 72 / xres - (box.ury - box.lly)) / 2;
        sprintf(retval, "90 rotate %d neg %d neg translate", llx, ury);
    }
    return retval;
}



static char *
compute_outfile_arg(const struct cmdline_info cmdline) {

    char *retval;  /* malloc'ed */

    retval = malloc(strlen(cmdline.input_filespec) + 10);

    if (cmdline.goto_stdout)
        strcpy(retval, "-");
    else if (strcmp(cmdline.input_filespec, "-") == 0)
        strcpy(retval, "-");
    else {
        strcpy(retval, cmdline.input_filespec);
        if (strlen(retval) > 3 && strcmp(retval+strlen(retval)-3, ".ps") == 0) 
            /* The input filespec ends in ".ps".  Chop it off. */
            retval[strlen(retval)-3] = '\0';

        strcat(retval, "%03d.");
        switch (cmdline.format_type) {
        case PBM_TYPE: strcat(retval, "pbm"); break;
        case PGM_TYPE: strcat(retval, "pgm"); break;
        case PPM_TYPE: strcat(retval, "ppm"); break;
        default: pm_error("Internal error: invalid value format_type");
        }
    }
    return(retval);
}



static char *
compute_gs_device(const int format_type, const int forceplain) {

    static char retval[20];

    switch (cmdline.format_type) {
    case PBM_TYPE: strcpy(retval, "pbm"); break;
    case PGM_TYPE: strcpy(retval, "pgm"); break;
    case PPM_TYPE: strcpy(retval, "ppm"); break;
    default: pm_error("Internal error: invalid value format_type");
    }
    if (!forceplain)
        strcat(retval, "raw");

    return(retval);
}



static void
execute_ghostscript(const char pstrans[], const char ghostscript_device[],
                    const char outfile_arg[], 
                    const int xsize, const int ysize, 
                    const int xres, const int yres,
                    const char input_filespec[], const int verbose) {

    int gs_exit;  /* wait4 exit code from Ghostscript */
    FILE *gs;  /* Pipe to Ghostscript's standard input */
    FILE *infile;
    int eof;  /* End of file on input */
    char ghostscript_command[250];

    if (strlen(outfile_arg) > 80)
        pm_error("output file spec too long.");
    
    sprintf(ghostscript_command,
            "gs -sDEVICE='%s' -sOutputFile='%s' -g'%dx%d' -r'%dx%d' "
            "-q -dNOPAUSE -",
            ghostscript_device, outfile_arg, 
            xsize, ysize, xres, yres);

    if (cmdline.verbose) {
        pm_message("Shell command: %s", ghostscript_command);
        pm_message("Postscript prefix command: '%s'", pstrans);
    }

    infile = pm_openr((char *)input_filespec);

    gs = popen(ghostscript_command, "w");
    if (gs == NULL) 
        pm_error("Unable to start Ghostscript shell command: %s\n",
                 ghostscript_command);
    
    fprintf(gs, "%s\n", pstrans);

    eof = FALSE;
    while (!eof) {
        char buffer[4096];
        int bytes_read;
        
        bytes_read = fread(buffer, 1, sizeof(buffer), infile);
        if (bytes_read == 0) 
            eof = TRUE;
        else 
            fwrite(buffer, 1, bytes_read, gs);
    }
    
    gs_exit = pclose(gs);
    pm_close(infile);

    if (gs_exit != 0) {
        if (WIFEXITED(gs_exit))
            pm_error("Ghostscript command failed.  Exit code=%d\n"
                     "Command was: %s", WEXITSTATUS(gs_exit), 
                     ghostscript_command);
        else if (WIFSIGNALED(gs_exit))
            pm_error("Ghostscript process died due to a signal %d.",
                     WTERMSIG(gs_exit));
        else 
            pm_error("Ghostscript process died with exit code %d", gs_exit);
    }
}



int
main(int argc, char **argv) {

    char *input_filespec;  /* malloc'ed */
        /* The file specification of our Postscript input file */
    int xres, yres;   /* Resolution in pixels per inch */
    int xsize, ysize;  /* output image size in pixels */
    struct box extract_box;
        /* coordinates of the box within the input we are to extract; i.e.
           that will become the output. 
           */
    struct box bordered_box;
        /* Same as above, but expanded to include borders */

    enum orientation orientation;
    char *ghostscript_device;
    char *outfile_arg;
    char *pstrans;

    pnm_init(&argc, argv);

    parse_command_line(argc, argv, &cmdline);

    add_ps_to_filespec(cmdline.input_filespec, &input_filespec,
                       cmdline.verbose);

    extract_box = compute_box_to_extract(cmdline.extract_box, input_filespec, 
                                         cmdline.verbose);

    orientation = compute_orientation(cmdline, extract_box);

    bordered_box = add_borders(extract_box, cmdline.xborder, cmdline.yborder,
                               cmdline.verbose);

    compute_size_res(cmdline, orientation, bordered_box, 
                     &xsize, &ysize, &xres, &yres);
    
    pstrans = compute_pstrans(bordered_box, orientation,
                              xsize, ysize, xres, yres);

    outfile_arg = compute_outfile_arg(cmdline);

    ghostscript_device = 
        compute_gs_device(cmdline.format_type, cmdline.forceplain);
    
    pm_message("Writing %s file", ghostscript_device);
    
    execute_ghostscript(pstrans, ghostscript_device, outfile_arg, 
                        xsize, ysize, xres, yres, input_filespec,
                        cmdline.verbose);

    return 0;
}


