/*
 * Copyright (C) 2005 Jeff Biseda <jbiseda@jbiseda.org>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA.
 */

/*
 * A little project to do some random image manipulation. I kept it to one
 * file so that's all you get.
 *
 * Use something like the following to compile:
 *   gcc -o hatchcross -lpng hatchcross.c
 *
 * Some good intro documentation on libpng:
 * http://www.libpng.org/pub/png/libpng-1.2.5-manual.html
 */

#include <stdlib.h>
#include <stdio.h>
#include <getopt.h>
#include <libpng/png.h>


#define bool char
#define FALSE 0
#define TRUE 1
#define SUCCESS(s) (0 == (s))
#define ARRAY_SIZE(x) (sizeof(x) / sizeof((x)[0]))


typedef struct _pixel
{
  png_byte r;
  png_byte g;
  png_byte b;
}
pixel_3_t;

typedef struct _pixel_4
{
  png_byte r;
  png_byte g;
  png_byte b;
  png_byte alpha;
}
pixel_4_t;

typedef struct _image_info_t
{
  png_bytep *rows;
  int width;
  int height;
  int rowbytes;
  png_byte color_type;
  png_byte bit_depth;
}
image_info_t;


inline void
swap ( int *x, int *y )
{
  *x ^= *y;
  *y ^= *x;
  *x ^= *y;
}

inline void
pixel_3_assign ( pixel_3_t *p1, pixel_3_t *p2 )
{
  p1->r = p2->r;
  p1->g = p2->g;
  p1->b = p2->b;
}

inline png_byte
pixel_3_set ( pixel_3_t *p, png_byte b )
{
  return p->r = p->g = p->b = b;
}

inline png_byte
pixel_3_gs ( pixel_3_t *p )
{
  return (p->r + p->g + p->b) / 3;
}


int
alloc_imgbuf ( png_bytep **rows, int height, int rowbytes );

void
free_imgbuf ( png_bytep *rows, int height );

int
read_png ( const char *filename, image_info_t *img );

int
write_png ( const char *filename,
            int width,
            int height,
            png_byte bit_depth,
            png_byte color_type,
            png_bytep *rows );

int
manipulate_image ( image_info_t *img, int min_contig_pix,
                   bool use_pass_2, bool overlay_orig );


void
version ()
{
  printf("hatchcross 0.1\n" \
         "Copyright (C) 2005 Jeff Biseda <jbiseda@jbiseda.org>\n" \
         "hatchcross comes with NO WARRANTY.\n" \
         "You may redistribute copies of hatchcross\n" \
         "under the terms of the GNU General Public License.\n");
}


void
help ( char **argv )
{
  printf("Usage: %s [OPTIONS] inputfile\n", argv[0]);
  printf("crosshatch png files\n");
  printf("\n");
  printf("  -o, --output=outputfile\n");
  printf("  -c, --contigpix=n\t\tdefault is 3\n");
  printf("  -2, --nopass2\t\t\tskip cleanup pass\n");
  printf("  -j, --overlay\t\t\toverlay original image\n");
  printf("  -h, --help\n");
  printf("  -v, --version\n");
  printf("\n");
  printf("Copyright (C) 2005 Jeff Biseda <jbiseda@jbiseda.org>\n");
}


int
main ( int argc, char **argv )
{
  int           status = 0;
  image_info_t  img;
  char          *ifilename = NULL;
  char          *ofilename = NULL;
  int           min_contig_pix = 3;
  int           option_index;
  bool          use_pass_2 = TRUE;
  bool          overlay_orig = FALSE;
  char          c;

  while ( TRUE )
  {
    int option_idx = 0;

    static struct option long_options[] =
      {
        {"output", 1, 0, 'o'},
        {"contigpix", 1, 0, 'c'},
        {"nopass2", 0, 0, '2'},
        {"overlay", 0, 0, 'j'},
        {"help", 0, 0, 'h'},
        {"version", 0, 0, 'v'},
        {0, 0, 0, 0}
      };

    c = getopt_long(argc, argv, "o:c:2jhv", long_options, &option_index);

    if ( -1 == c )
      break;

    switch ( c )
    {
    case 0:
      break;
      
    case 'o':
      ofilename = optarg;
      break;

    case 'c':
      min_contig_pix = atoi(optarg);
      break;

    case '2':
      use_pass_2 = FALSE;
      break;

    case 'j':
      overlay_orig = TRUE;
      break;

    case 'h':
      help(argv);
      status = 1;
      break;

    case 'v':
      version();
      status = 1;
      break;

    default:
      status = 1;
      break;
    }
  }

  argc -= optind;
  argv += optind;

  if ( SUCCESS(status) && argc == 1 )
    ifilename = argv[0];

  if ( SUCCESS(status) && (!ifilename || !ofilename) )
  {
    help(argv);
    status = 1;
  }

  if ( SUCCESS(status) )
    status = read_png(ifilename, &img);

  if ( SUCCESS(status) )
    status = manipulate_image(&img, min_contig_pix, use_pass_2, overlay_orig);

  if ( SUCCESS(status) )
    status = write_png(ofilename,
                       img.width,
                       img.height,
                       img.bit_depth,
                       img.color_type,
                       img.rows);

  return status;
}


int
alloc_imgbuf ( png_bytep **rows, int height, int rowbytes )
{
  int i;
  int status = 0;

  *rows = malloc(sizeof(png_bytep) * height);

  if ( !*rows ) status = 1;

  if ( SUCCESS(status) )
    for ( i = 0; i < height; i++ )
    {
      (*rows)[i] = malloc(rowbytes);
      if ( !(*rows)[i] )
      {
        status = 1;
        break;
      }
    }

  return status;
}


void
free_imgbuf ( png_bytep *rows, int height )
{
  int i;

  if ( rows )
  {
    for ( i = 0; i < height; i++ )
      if ( rows[i] )
        free(rows[i]);

    free(rows);
  } 
}


int
read_png ( const char *filename, image_info_t *img )
{
  const int header_len = 8;
  FILE *fp = NULL;
  png_byte header[8];
  png_structp png_ptr;
  png_infop info_ptr;
  png_infop end_info;
  int status = 0;
  int i;

  bzero(img, sizeof(image_info_t));

  fp = fopen(filename, "rb");

  if ( !fp )
  {
    fprintf(stderr, "error opening file <%s>\n", filename);
    status = 1;
  }

  if ( SUCCESS(status) )
  {
    if ( fread(header, sizeof(png_byte), header_len, fp) != header_len )
    {
      fprintf(stderr, "failed to read png header from file\n");
      status = 1;
    }
  }

  if ( SUCCESS(status) )
  {
    if ( png_sig_cmp(header, 0, 8) )
    {
      fprintf(stderr, "not a png file\n");
      status = 1;
    }
  }

  if ( SUCCESS(status) )
  {
    png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING,
                                     NULL, NULL, NULL);
    
    if ( !png_ptr ) status = 1;
  }

  if ( SUCCESS(status) )
  {
    if ( setjmp(png_jmpbuf(png_ptr)) )
      status = 1;
    else
      info_ptr = png_create_info_struct(png_ptr);

    if ( !info_ptr ) status = 1;
  }

  if ( SUCCESS(status) )
  {
    if ( setjmp(png_jmpbuf(png_ptr)) )
      status = 1;
    else
      end_info = png_create_info_struct(png_ptr);

    if ( !end_info ) status = 1;
  }

  if ( SUCCESS(status) )
  {
    if ( setjmp(png_jmpbuf(png_ptr)) )
      status = 1;
    else
      png_init_io(png_ptr, fp);
  }

  if ( SUCCESS(status) )
  {
    /* inform libpng that we already read 8 bytes of header from the file */
    if ( setjmp(png_jmpbuf(png_ptr)) )
      status = 1;
    else
      png_set_sig_bytes(png_ptr, 8);
  }

  if ( SUCCESS(status) )
  {
    if ( setjmp(png_jmpbuf(png_ptr)) )
      status = 1;
    else
      png_read_info(png_ptr, info_ptr);
  }

  if ( SUCCESS(status) )
  {
    if ( setjmp(png_jmpbuf(png_ptr)) )
      status = 1;
    else
      png_read_update_info(png_ptr, info_ptr);
  }

  if ( SUCCESS(status) )
  {
    img->width = info_ptr->width;
    img->height = info_ptr->height;
    img->color_type = info_ptr->color_type;
    img->bit_depth = info_ptr->bit_depth;
    img->rowbytes = info_ptr->rowbytes;
    
    img->rows = (png_bytep*)malloc(sizeof(png_bytep) * info_ptr->height);

    if ( !img->rows )
    {
      status = 1;
    }
    else
    {
      bzero(img->rows, sizeof(png_bytep) * info_ptr->height);

      for ( i = 0; i < info_ptr->height; i++ )
      {
        img->rows[i] = (png_byte*)malloc(info_ptr->rowbytes);

        if ( !img->rows[i] ) status = 1;
      }
    }
  }

  if ( SUCCESS(status) )
  {
    if ( setjmp(png_jmpbuf(png_ptr)) )
      status = 1;
    else
      png_read_image(png_ptr, img->rows);
  }

  if ( SUCCESS(status) )
  {
    if ( setjmp(png_jmpbuf(png_ptr)) )
      status = 1;
    else
      png_read_end(png_ptr, end_info);
  }

  if ( SUCCESS(status) )
  {
    printf("<%s> %d x %d, color type %d, bit depth %d\n",
           filename, img->width, img->height,
           img->color_type, img->bit_depth);

    for ( i = 0; i < info_ptr->num_text; i++ )
      printf("<%s> %s\n", info_ptr->text[i].key, info_ptr->text[i].text);
    for ( i = 0; i < end_info->num_text; i++ )
      printf("<%s> %s\n", end_info->text[i].key, end_info->text[i].text);
  }
    
  png_destroy_read_struct(&png_ptr, &info_ptr, &end_info);

  if ( !SUCCESS(status) && img->rows )
  {
    for ( i = 0; i < info_ptr->height; i++ )
      if ( img->rows[i] )
        free(img->rows[i]);

    free(img->rows);
  }

  if ( fp ) fclose(fp);

  return status;
}


int
write_png ( const char *filename,
            int width,
            int height,
            png_byte bit_depth,
            png_byte color_type,
            png_bytep *rows )
{
  png_text      texts[0];
  png_structp   png_ptr;
  png_infop     info_ptr;
  FILE          *fp = NULL;
  int           status = 0;

  texts[0].compression = PNG_TEXT_COMPRESSION_NONE;
  texts[0].key = "Software";
  texts[0].text = "lines software";
  texts[0].text_length = strlen(texts[0].text) + sizeof(char);

  fp = fopen(filename, "wb");

  if ( !fp )
  {
    fprintf(stderr, "could not open file <%s> for write\n", filename);
    status = 1;
  }

  if ( SUCCESS(status) )
  {
    png_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);
    info_ptr = png_create_info_struct(png_ptr);

    if ( !png_ptr || !info_ptr )
      status = 1;
  }

  if ( SUCCESS(status) )
  {
    if ( setjmp(png_jmpbuf(png_ptr)) )
      status = 1;
    else
      png_init_io(png_ptr, fp);
  }

  if ( SUCCESS(status) )
  {
    if ( setjmp(png_jmpbuf(png_ptr)) )
      status = 1;
    else
      png_set_IHDR(png_ptr,
                   info_ptr,
                   width,
                   height,
                   bit_depth,
                   color_type,
                   PNG_INTERLACE_NONE,
                   PNG_COMPRESSION_TYPE_BASE,
                   PNG_FILTER_TYPE_BASE);
  }

  if ( SUCCESS(status) )
  {
    if ( setjmp(png_jmpbuf(png_ptr)) )
      status = 1;
    else
      png_set_text(png_ptr, info_ptr, texts, ARRAY_SIZE(texts));
  }

  if ( SUCCESS(status) )
  {
    if ( setjmp(png_jmpbuf(png_ptr)) )
      status = 1;
    else
      png_write_info(png_ptr, info_ptr);
  }

  if ( SUCCESS(status) )
  {
    if ( setjmp(png_jmpbuf(png_ptr)) )
      status = 1;
    else    
      png_write_image(png_ptr, rows);
  }

  if ( SUCCESS(status) )
  {
    if ( setjmp(png_jmpbuf(png_ptr)) )
      status = 1;
    else    
      png_write_end(png_ptr, NULL);
  }

  png_destroy_write_struct(&png_ptr, &info_ptr);

  if ( fp ) fclose(fp);

  return status;
}


int
manipulate_image ( image_info_t *img, int min_contig_pix,
                   bool use_pass_2, bool overlay_orig )
{
  png_bytep     *rows = NULL;
  png_bytep     *rows2 = NULL;
  png_bytep     *tmprows = NULL;
  png_byte      *irow;
  png_byte      *orow;
  pixel_3_t     *ip;
  pixel_3_t     *op;
  int           line_avg;
  int           count;
  int           x;
  int           y;
  int           i;
  int           status = 0;
  int           pixel_bytes = (img->color_type & PNG_COLOR_MASK_ALPHA)
    ? sizeof(pixel_4_t) : sizeof(pixel_3_t);

  status = alloc_imgbuf(&rows, img->height, img->rowbytes);

  if ( use_pass_2 && SUCCESS(status) )
    status = alloc_imgbuf(&rows2, img->height, img->rowbytes);

  /* generate the new image */
  if ( SUCCESS(status) )
  {
    /* set the output to white */
    for ( y = 0; y < img->height; y++ )
      for ( x = 0; x < img->width; x++ )
      {
        op = (pixel_3_t *)&rows[y][x * pixel_bytes];
        op->r = op->g = op->b = ~0;
        if ( img->color_type & PNG_COLOR_MASK_ALPHA )
          ((pixel_4_t *)op)->alpha = ~0;

        if ( use_pass_2 )
        {
          op = (pixel_3_t *)&rows2[y][x * pixel_bytes];
          op->r = op->g = op->b = ~0;
          if ( img->color_type & PNG_COLOR_MASK_ALPHA )
            ((pixel_4_t *)op)->alpha = ~0;
        }
      }

    /* fill in pixels across rows (pass 1) */
    for ( y = 0; y < img->height; y += 2 )
    {
      for ( line_avg = 0, x = 0; x < img->width; x++ )
      {
        ip = (pixel_3_t *)&img->rows[y][x * pixel_bytes];
        line_avg += pixel_3_gs(ip);
      }
      line_avg /= img->width;

      for ( x = 0; x < img->width; x++ )
      {
        ip = (pixel_3_t *)&img->rows[y][x * pixel_bytes];
        op = (pixel_3_t *)&rows[y][x * pixel_bytes];

        if ( pixel_3_gs(ip) < line_avg )
          op->r = op->g = op->b = 0;
      }
    }

    /* fill in pixels across cols (pass 1) */
    for ( x = 0; x < img->width; x += 2 )
    {
      for ( line_avg = 0, y = 0; y < img->height; y++ )
      {
        ip = (pixel_3_t *)&img->rows[y][x * pixel_bytes];
        line_avg += pixel_3_gs(ip);
      }
      line_avg /= img->height;

      for ( y = 0; y < img->height; y++ )
      {
        ip = (pixel_3_t *)&img->rows[y][x * pixel_bytes];
        op = (pixel_3_t *)&rows[y][x * pixel_bytes];

        if ( pixel_3_gs(ip) < line_avg )
          op->r = op->g = op->b = 0;
      }
    }

    if ( use_pass_2 )
    {
      /* cleanup rows (pass 2) */
      for ( y = 0; y < img->height; y += 2 )
      {
        for ( x = 0; x < img->width; x++ )
        {
          /* ignore non black pixels */
          if ( 0 != pixel_3_gs((pixel_3_t *)&rows[y][x * pixel_bytes]) )
            continue;

          /* find leftmost contiguous black pixel */
          for ( i = x; i >= 0; i-- )
            if ( 0 != pixel_3_gs((pixel_3_t *)&rows[y][i * pixel_bytes]) )
            {
              i++;
              break;
            }
          if ( i < 0 ) i = 0;

          /* count contiguous pixels */
          for ( count = 0; i < img->width; i++, count++ )
            if ( 0 != pixel_3_gs((pixel_3_t *)&rows[y][i * pixel_bytes]) )
              break;

          if ( count >= min_contig_pix )
            pixel_3_set((pixel_3_t *)&rows2[y][x * pixel_bytes], 0);
        }
      }

      /* cleanup cols (pass 2) */
      for ( x = 0; x < img->width; x += 2 )
      {
        for ( y = 0; y < img->height; y++ )
        {
          /* ignore non black pixels */
          if ( 0 != pixel_3_gs((pixel_3_t *)&rows[y][x * pixel_bytes]) )
            continue;

          /* find topmost contiguous black pixel */
          for ( i = y; i >= 0; i-- )
            if ( 0 != pixel_3_gs((pixel_3_t *)&rows[i][x * pixel_bytes]) )
            {
              i++;
              break;
            }
          if ( i < 0 ) i = 0;

          /* count contiguous pixels */
          for ( count = 0; i < img->height; i++, count++ )
            if ( 0 != pixel_3_gs((pixel_3_t *)&rows[i][x * pixel_bytes]) )
              break;

          if ( count >= min_contig_pix )
            pixel_3_set((pixel_3_t *)&rows2[y][x * pixel_bytes], 0);
        }
      }

      free_imgbuf(rows, img->height);

      rows = rows2;
    }

    if ( overlay_orig )
    {
      /* overlay the new image on the old image */
      for ( y = 0; y < img->height; y++ )
        for ( x = 0; x < img->width; x++ )
        {
          ip = (pixel_3_t *)&img->rows[y][x * pixel_bytes];
          op = (pixel_3_t *)&rows[y][x * pixel_bytes];
          
          if ( op->r + op->g + op->b != 0 )
            pixel_3_assign(op, ip);
        }
    }
  }

  /* swap the image buffers */
  if ( SUCCESS(status) )
  {
    tmprows = img->rows;
    img->rows = rows;

    free_imgbuf(tmprows, img->height);
  }
  else
  {
    free_imgbuf(rows, img->height);
    free_imgbuf(rows2, img->height);
  }

  return status;
}
