/* pnmcrop.c - crop a portable anymap
**
** Copyright (C) 1988 by Jef Poskanzer.
**
** Permission to use, copy, modify, and distribute this software and its
** documentation for any purpose and without fee is hereby granted, provided
** that the above copyright notice appear in all copies and that both that
** copyright notice and this permission notice appear in supporting
** documentation.  This software is provided "as is" without express or
** implied warranty.
*/

/* IDEA FOR EFFICIENCY IMPROVEMENT:

   If we have to read the input into a regular file because it is not
   seekable (a pipe), find the borders as we do the copy, so that we
   do 2 passes through the file instead of 3.
*/

#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include "pnm.h"

enum bg_choice {BG_BLACK, BG_WHITE, BG_DEFAULT};

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 */
    enum bg_choice background;
    int left, right, top, bottom;
    int verbose;
} 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 black_opt, white_opt;
    
    option_def_index = 0;   /* incremented by OPTENTRY */
    OPTENTRY(0,   "black",      OPT_FLAG,   &black_opt,                 0);
    OPTENTRY(0,   "white",      OPT_FLAG,   &white_opt,                 0);
    OPTENTRY(0,   "left",       OPT_FLAG,   &cmdline_p->left,           0);
    OPTENTRY(0,   "right",      OPT_FLAG,   &cmdline_p->right,          0);
    OPTENTRY(0,   "top",        OPT_FLAG,   &cmdline_p->top,            0);
    OPTENTRY(0,   "bottom",     OPT_FLAG,   &cmdline_p->bottom,         0);
    OPTENTRY(0,   "verbose",    OPT_FLAG,   &cmdline_p->verbose,        0);

    /* Set the defaults */
    cmdline_p->left = cmdline_p->right = cmdline_p->top = cmdline_p->bottom 
        = FALSE;
    cmdline_p->verbose = FALSE;
    black_opt = FALSE;
    white_opt = FALSE;

    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 input filespec", argc-1);

    if (black_opt && white_opt)
        pm_error("You cannot specify both -black and -white");
    else if (black_opt)
        cmdline_p->background = BG_BLACK;
    else if (white_opt)
        cmdline_p->background = BG_WHITE;
    else
        cmdline_p->background = BG_DEFAULT;

    if (!cmdline_p->left && !cmdline_p->right && !cmdline_p->top
        && !cmdline_p->bottom) {
        cmdline_p->left = cmdline_p->right = cmdline_p->top 
            = cmdline_p->bottom = TRUE;
    }
}



static void
make_seekable_file(FILE *input_file, FILE **seekable_file_p) {
/*----------------------------------------------------------------------------
   We need to return an open file which is seekable -- I.e. we can
   rewind it.  If the input file is seekable, then we can just pass it
   back.  Otherwise, we must create a temporary regular file and copy
   the input file to it, then close the input file and return the
   handle of the temporary file.  We use a file that the operating
   system recognizes as temporary, so it picks the filename and
   deletes the file when we close it.
-----------------------------------------------------------------------------*/
    int stat_rc;
    int seekable;  /* logical: file is seekable */
    struct stat statbuf;

    /* I would use fseek() to determine if the file is seekable and 
       be a little more general than checking the type of file, but I
       don't have reliable information on how to do that.  I have seen
       streams be partially seekable -- you can, for example seek to
       0 if the file is positioned at 0 but you can't actually back up
       to 0.  I have seen documentation that says the errno for an
       unseekable stream is EBADF and in practice seen ESPIPE.

       On the other hand, regular files are always seekable and even if
       some other file is, it doesn't hurt much to assume it isn't.
    */

    stat_rc = fstat(fileno(input_file), &statbuf);
    if (stat_rc == 0 && S_ISREG(statbuf.st_mode))
        seekable = TRUE;
    else 
        seekable = FALSE;

    if (seekable) {
        *seekable_file_p = input_file;
    } else {
        *seekable_file_p = tmpfile();

        /* Copy the input into the temporary seekable file */
        while (!feof(input_file) && !ferror(input_file) 
               && !ferror(*seekable_file_p)) {
            char buffer[4096];
            int bytes_read;
            bytes_read = fread(buffer, 1, sizeof(buffer), input_file);
            fwrite(buffer, 1, bytes_read, *seekable_file_p);
        }
        if (ferror(input_file))
            pm_error("Error reading input file into temporary file.  "
                     "Errno = %s (%d)", strerror(errno), errno);
        if (ferror(*seekable_file_p))
            pm_error("Error writing input into temporary file.  "
                     "Errno = %s (%d)", strerror(errno), errno);
        pm_close(input_file);
        {
            int seek_rc;
            seek_rc = fseek(*seekable_file_p, 0, SEEK_SET);
            if (seek_rc != 0)
                pm_error("fseek() failed to rewind temporary file.  "
                         "Errno = %s (%d)", strerror(errno), errno);
        }
    }
}



static xel
compute_background(const enum bg_choice background_choice,
                   FILE * image_file, const int cols, 
                   const xelval maxval, const int format) {

    xel background;  /* Our return value */
    
    switch (background_choice) {
    case BG_WHITE:
	    background = pnm_whitexel(maxval, format);
        break;
    case BG_BLACK:
	    background = pnm_blackxel(maxval, format);
        break;
    case BG_DEFAULT: {
        int filepos;
        int seek_rc;
        xel *xelrow;

        filepos = ftell(image_file);
        if (filepos < 0)
            pm_error("ftell() was unable to tell the position of the file, "
                     "for purposes of resetting after checking the "
                     "background color.  Errno = %s (%d)",
                     strerror(errno), errno);

        xelrow = pnm_allocrow(cols);
        pnm_readpnmrow(image_file, xelrow, cols, maxval, format);
        background = pnm_backgroundxelrow(xelrow, cols, maxval, format);
        pnm_freerow(xelrow);

        seek_rc = fseek(image_file, filepos, SEEK_SET);
        if (seek_rc < 0) 
            pm_error("lseek() failed to reposition file after checking "
                     "background color.  "
                     "Errno = %s (%d)", strerror(errno), errno);
        
        break;
    }
    }
    return(background);
}



static void
find_borders(FILE *image_file, const enum bg_choice background_choice, 
             int * const left_p, int * const right_p, 
             int * const top_p, int * const bottom_p) {
/*----------------------------------------------------------------------------
   Find the left, right, top, and bottom borders in the image
   'image_file'.  Return as *left_p the column number of the leftmost
   column that contains something besides background color.  Return as
   *right_p the column number of the rightmost such column.  Return as
   *top_p the row number of the topmost not-all-background row, and
   return as *bottom_p the bottommost such row.
   
   Note that these are rown and column numbers, not border sizes, and
   all refer to a row or column that is part of the image, not the
   border.

   Iff the image is all background, *right_p == -1.

   Expect the input file to be positioned to the beginning of an image
   and leave it positioned just after that image.
-----------------------------------------------------------------------------*/

    xel* xelrow;        /* A row of the input image */
    xel background;
    xelval maxval;
    int format;
    int rows, cols;
    int row, gottop;

    pnm_readpnminit(image_file, &cols, &rows, &maxval, &format);

    background = compute_background(background_choice, image_file,
                                    cols, maxval, format);

    xelrow = pnm_allocrow(cols);
    
    *left_p = cols;  /* initial value */
    *right_p = -1;   /* initial value */
    *top_p = rows;   /* initial value */
    *bottom_p = -1;  /* initial value */

    gottop = FALSE;
    for (row = 0; row < rows; row++) {
        int col;
        int gotleft;
        int this_row_right, this_row_left;

        gotleft = FALSE;  /* initial value */
        this_row_right = -1;  /* initial value */
        this_row_left = cols;
        pnm_readpnmrow(image_file, xelrow, cols, maxval, format);
        for (col = 0; col < cols; col++) {
            if (!PNM_EQUAL(xelrow[col], background)) {
                if (!gotleft) {
                    gotleft = TRUE;
                    this_row_left = col;
                }
                this_row_right = col;   /* New candidate */
            }
        }
        *right_p = max(this_row_right, *right_p);
        *left_p = min(this_row_left, *left_p);

        if (this_row_left < cols) {
            /* This row is not entirely background */
            if (!gottop) {
                gottop = TRUE;
                *top_p = row;
            }
            *bottom_p = row;   /* New candidate */
        }
    }
}



static void
report_cropping_parameters(const int left, const int right, 
                           const int top, const int bottom,
                           const int rows, const int cols) {

#define ending(n) (((n) > 1) ? "s" : "")

    if (top > 0)
        pm_message("cropping %d row%s off the top", top, ending(top));
    if (bottom < rows - 1)
        pm_message("cropping %d row%s off the bottom", 
                   rows-1-bottom, ending(rows-1-bottom));
    if (left > 0)
        pm_message("cropping %d col%s off the left", left, ending(left));
    if (right < cols - 1)
        pm_message("cropping %d col%s off the right", 
                    cols-1-right, ending(cols-1-right));

    if (top == 0 && bottom == rows-1 && left == 0 && right == cols-1) {
        pm_message("Not cropping.  No border found.");
    }
}



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

    FILE* ifp;   /* The program's input file */
    FILE* image_file;
        /* A seekable image file containing our input image.  It may be
           the same as 'ifp', but if 'ifp' is not seekable, is another
           file containing the same data.
           */
    xelval maxval;
    int format;
    int rows, cols;   /* dimensions of input image */
    int left, right, top, bottom;
        /* The places at which we crop */
    int left_border, right_border, top_border, bottom_border;
        /* The locations of the borders in the input image */
    int seek_rc;  /* return code from a file seek operation */

    pnm_init(&argc, argv);

    parse_command_line(argc, argv, &cmdline);

    ifp = pm_openr(cmdline.input_filespec);

    make_seekable_file(ifp, &image_file);

    find_borders(image_file, cmdline.background, &left_border, &right_border, 
                 &top_border, &bottom_border);

    if (right_border == -1) {
        pm_error("The image is entirely background; "
                 "there is nothing to crop.");
    }

    seek_rc = fseek(image_file, 0, SEEK_SET);
    if (seek_rc != 0) 
        pm_error("fseek() failed to reset the image file to the beginning "
                 "after examining it for borders.  Errno = %s (%d)",
                 strerror(errno), errno);
    
    pnm_readpnminit(image_file, &cols, &rows, &maxval, &format);
    
    if (cmdline.left) left = left_border;
    else left = 0;
    if (cmdline.right) right = right_border;
    else right = cols-1;
    if (cmdline.top) top = top_border;
    else top = 0;
    if (cmdline.bottom) bottom = bottom_border;
    else bottom = rows-1;
        
    if (cmdline.verbose) 
        report_cropping_parameters(left, right, top, bottom, rows, cols);

    {
        /* Now we just do a Pnmcut */
        int row;
        int newcols, newrows;
        xel *xelrow;

        xelrow = pnm_allocrow(cols);

        newcols = right - left + 1;
        newrows = bottom - top + 1;
        pnm_writepnminit(stdout, newcols, newrows, maxval, format, 0);
        for (row = 0; row < rows; row++) {
            pnm_readpnmrow(image_file, xelrow, cols, maxval, format);
            if (row >= top && row <= bottom)
                pnm_writepnmrow(stdout, &(xelrow[left]), newcols, 
                                maxval, format, 0);
        }
        pnm_freerow(xelrow);
    }

    pm_close(stdout);
    pm_close(image_file);

    exit(0);
    }
