/*******************************************************************
 * JPEGoptim
 * Copyright (c) Timo Kokkonen, 1996.
 *
 * requires libjpeg.a (from JPEG Group's JPEG software 
 *                     release 6a or later...)
 *
 * to compile type: gcc -o jpegoptim jpegoptim.c -ljpeg
 */

#include <stdio.h>
#include <stdlib.h>
#include <sys/param.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <dirent.h>
#ifndef HPUX
#ifndef AMIGA
#include <getopt.h>
#endif
#endif
#include <signal.h>
#include <string.h>
#include <jpeglib.h>
#include <setjmp.h>
#ifdef SGI
#include <libgen.h>
#endif

#define VERSIO "1.1"

#ifdef AMIGA
#define TEMPDIR "t:"
#define TEMP2DIR "t:"
#else
#define TEMPDIR "."
#define TEMP2DIR "/tmp"
#endif

#ifdef SGI
#undef METHODDEF
#define METHODDEF(x) static x
#endif


struct my_error_mgr {
  struct jpeg_error_mgr pub;
  jmp_buf setjmp_buffer;   
};

typedef struct my_error_mgr * my_error_ptr;

struct jpeg_decompress_struct dinfo;
struct jpeg_compress_struct cinfo;
struct my_error_mgr jcerr,jderr;

#ifdef LONG_OPTIONS
struct option long_options[] = {
  {"verbose",0,0,'v'},
  {"help",0,0,'h'},
  {"quiet",0,0,'q'},
  {"max",1,0,'m'},
  {"totals",0,0,'t'},
  {"noaction",0,0,'n'},
  {"dest",1,0,'d'},
  {"force",0,0,'f'},
  {0,0,0,0}
};
#endif

int verbose_mode = 0;
int quiet_mode = 0;
int global_error_counter = 0;
char *outfname = NULL;
FILE *infile = NULL, *outfile = NULL;

#ifdef AMIGA
const char dummie[]="$VER: jpegoptim " VERSIO ;
#endif

/*****************************************************************/

METHODDEF(void) 
my_error_exit (j_common_ptr cinfo)
{
  my_error_ptr myerr = (my_error_ptr)cinfo->err;
  (*cinfo->err->output_message) (cinfo);
  longjmp(myerr->setjmp_buffer,1);
}

METHODDEF(void)
my_output_message (j_common_ptr cinfo)
{
  char buffer[JMSG_LENGTH_MAX];

  (*cinfo->err->format_message) (cinfo, buffer); 
  printf(" %s ",buffer);
  global_error_counter++;
}
	
#ifdef AMIGA /* some sucking replacement routines */
int crap_rename(char *vanha, char *uusi)
{
    unsigned char *buffer;
    FILE *fp;
    FILE *dp;
    unsigned long luettu=0;
    int c;

    if((buffer=malloc(32000)))
    {
      if((fp=fopen(vanha, "rb")))
      {
         if((dp=fopen(uusi, "wb")))
         {
            while((c=fgetc(fp))!=EOF)            
            {                                    
                ungetc(c, fp);               
                luettu=fread(buffer, 1, 32000, fp);
                fwrite(buffer,1,luettu,dp);
            }
			fclose(dp);
         }
         else 
         {
            printf("Error opening %s for writing.\n", uusi);
            return(1);
         }
		 fclose(fp);
      }
      else 
      { 
         printf("Error opening %s for reading.\n", vanha);
         return(1);
      }
      free(buffer);
    }
    else 
    {
      printf("Can't allocate 32000 bytes of mem.\n");
      return(1);
    }

    unlink(vanha);

    return(0);
}

char *realpath __P((const char *jorma, char *keijo))
{
  int c;

  if(!jorma) return(NULL);

  strcpy(keijo, jorma);

  c=strlen(keijo)-1;

  if(keijo[c]!=':' && keijo[c]!='/')
  {
	keijo[c+1]='/';
 	keijo[c+2]=NULL;
  }

  return(keijo);
}

char *basename(char *polku)
{
  char *temppi;

  while((temppi=strchr(polku,':')))
  {
    polku=temppi+1;
  }

  while((temppi=strchr(polku,'/')))
  {
    polku=temppi+1;
  }

  return(polku);
}
#endif

void no_memory(void)
{
  if (!quiet_mode) fprintf(stderr,"jpegoptim: not enough memory.\n");
  exit(3);
}

void p_usage(void) 
{
 if (!quiet_mode) {
  fprintf(stderr,"jpegoptim " VERSIO 
	  " Copyright (c) Timo Kokkonen, 1996.\n"); 

  fprintf(stderr,
       "Usage: jpegoptim [options] <filenames> \n\n"
#ifdef LONG_OPTIONS
       "  -d<path>, --dest=<path>\n"
       "                 specify alternative destination directory for \n"
       "                 optimized files (default is to overwrite originals)\n"
       "  -f, --force    force optimization\n"
       "  -h, --help     display this help and exit\n"
       "  -m[0..100], --max=[0..100] \n"
       "                 set maximum image quality factor (disables lossless\n"
       "                 optimization mode, which is by default on)\n"
       "  -n, --noaction don't really optimize files, just print results\n"
       "  -q, --quiet    quiet mode\n"
       "  -t, --totals   print totals after processing all files\n"
       "  -v, --verbose  enable verbose mode (positively chatty)\n"
#else
       "  -d<path>       specify alternative destination directory for \n"
       "                 optimized files (default is to overwrite originals)\n"
       "  -f             force optimization\n"
       "  -h             display this help and exit\n"
       "  -m[0..100]     set maximum image quality factor (disables lossless\n"
       "                 optimization mode, which is by default on)\n"
       "  -n             don't really optimize files, just print results\n"
       "  -q             quiet mode\n"
       "  -t             print totals after processing all files\n"
       "  -v             enable verbose mode (positively chatty)\n"
#endif
       "\n\n");
 }

 exit(1);
}

int delete_file(char *name)
{
  int retval;

  if (verbose_mode&&!quiet_mode) fprintf(stderr,"deleting: %s\n",name);
  if ((retval=unlink(name))&&!quiet_mode) {
    fprintf(stderr,"jpegoptim: error removing file: %s\n",name);
  }

  return retval;
}

int file_size(FILE *fp)
{
  long size=0,save=0;
  
  fgetpos(fp,&save);
  fseek(fp,0L,SEEK_END);
  fgetpos(fp,&size);
  fseek(fp,save,SEEK_SET);
  
  return size;
}

int is_directory(const char *path)
{
  DIR *dir = opendir(path);

  if (!dir) {
    return 1;
  }
  closedir(dir);
  return 0;
}


int is_dir(FILE *fp)
{
 struct stat buf;
 if (fstat(fileno(fp),&buf)) {
   fprintf(stderr,"jpeginfo: fstat() failed.\n");
   exit(3);
 }
 
 if (S_ISDIR(buf.st_mode)) return 1;

 return 0;
}


int file_exists(const char *pathname)
{
  FILE *file=fopen(pathname,"r");
  
  if (!file) return 0;
  fclose(file);
  return 1;
}

void own_signal_handler(int a)
{
  if (verbose_mode) printf("jpegoptim: signal: %d\n",a);
  if (outfile) fclose(outfile);
  if (outfname) if (file_exists(outfname)) delete_file(outfname);
  exit(1);
}

/*****************************************************************/
int main(int argc, char **argv) 
{
  JSAMPARRAY buf = NULL;
  char name1[MAXPATHLEN],name2[MAXPATHLEN];
  char newname[MAXPATHLEN], dest_path[MAXPATHLEN];
  jvirt_barray_ptr *coef_arrays=NULL;
  int c,i,j,lines_read, err_count;
  int totals_mode = 0;
  int opt_index = 0;
  int quality = -1;
  int noaction = 0;
  int retry = 0;
  int dest = 0;
  int force = 0;
  long insize,outsize,average_count=0;
  double ratio,average_rate = 0.0,total_save=0.0;
  pid_t cur_pid = getpid();
  uid_t cur_uid = getuid();

#ifdef AMIGA
  sprintf(name1,TEMPDIR "jpegoptim.%06x.%04x.tmp",cur_pid,cur_uid);
  sprintf(name2,TEMP2DIR "jpegopti2.%06x.%04x.tmp",cur_pid,cur_uid);
#else
  sprintf(name1,TEMPDIR "/jpegoptim.%06x.%04x.tmp",cur_pid,cur_uid);
  sprintf(name2,TEMP2DIR "/jpegoptim.%06x.%04x.tmp",cur_pid,cur_uid);
#endif

  signal(SIGINT,own_signal_handler);
  signal(SIGTERM,own_signal_handler);

  /* initialize decompression object */
  dinfo.err = jpeg_std_error(&jderr.pub);
  jpeg_create_decompress(&dinfo);
  jderr.pub.error_exit=my_error_exit;
  jderr.pub.output_message=my_output_message;

  /* initialize compression object */
  cinfo.err = jpeg_std_error(&jcerr.pub);
  jpeg_create_compress(&cinfo);
  jcerr.pub.error_exit=my_error_exit;
  jcerr.pub.output_message=my_output_message;



  if (argc<2) {
    if (!quiet_mode) fprintf(stderr,"jpegoptim: file arguments missing\n"
			     "Try 'jpegoptim "
#ifdef LONG_OPTIONS
			     "--help"
#else
			     "-h"
#endif
			     "' for more information.\n");
    exit(1);
  }
 
  /* parse command line parameters */
  while(1) {
    opt_index=0;
#ifdef LONG_OPTIONS
    if ((c=getopt_long(argc,argv,"d:hm:ntqvf",long_options,&opt_index))==-1) 
#else
    if ((c=getopt(argc,argv,"d:hm:ntqvf"))==-1) 
#endif
      break;
    switch (c) {
    case 'm':
      {
        int tmpvar;

        if (sscanf(optarg,"%d",&tmpvar) == 1) {
	  quality=tmpvar;
	  if (quality < 0) quality=0;
	  if (quality > 100) quality=100;
	}
	else if (!quiet_mode) {
	  fprintf(stderr,"jpegoptim: invalid argument for -m, --max\n");
	  exit(1);
	}
      }
      break;
    case 'd':
      if (realpath(optarg,dest_path)==NULL || is_directory(dest_path)) {
	fprintf(stderr,"jpegoptim: invalid argument for -d, --dest\n");
	exit(1);
      }
      if (verbose_mode) 
	fprintf(stderr,"Destination directory: %s\n",dest_path);
      dest=1;
      break;
    case 'v':
      verbose_mode=1;
      break;
    case 'h':
      p_usage();
      break;
    case 'q':
      quiet_mode=1;
      break;
    case 't':
      totals_mode=1;
      break;
    case 'n':
      noaction=1;
      break;
    case 'f':
      force=1;
      break;
    case '?':
      break;

    default:
      if (!quiet_mode) 
	fprintf(stderr,"jpegoptim: error parsing parameters.\n");
    }
  }


  if (verbose_mode && (quality>0)) 
    fprintf(stderr,"Image quality limit set to: %d\n",quality);


  /* loop to process the input files */
  i=1;  
  do {

   if (!argv[i][0]) continue;
   if (argv[i][0]=='-') continue;

  retry_point:
   if ((infile=fopen(argv[i],"r"))==NULL) {
     if (!quiet_mode) fprintf(stderr, "jpegoptim: can't open %s\n", argv[i]);
     continue;
   }
   if (is_dir(infile)) {
     fclose(infile);
     if (verbose_mode) printf("directory: %s  skipped\n",argv[i]); 
     continue;
   }

   /* setup error handling for decompress */
   if (setjmp(jderr.setjmp_buffer)) {
      jpeg_abort_decompress(&dinfo);
      fclose(infile);
      printf(" [ERROR]\n");
      continue;
   }

   if (!retry) { printf("%s ",argv[i]); fflush(stdout); }

   /* prepare to decompress */
   global_error_counter=0;
   err_count=jderr.pub.num_warnings;
   jpeg_stdio_src(&dinfo, infile);
   jpeg_read_header(&dinfo, TRUE); 

   if (!retry) {
     printf("%dx%d %dbit ",(int)dinfo.image_width,
	    (int)dinfo.image_height,(int)dinfo.num_components*8);
     if (dinfo.saw_Adobe_marker) printf("Adobe ");
     else if (dinfo.saw_JFIF_marker) printf("JFIF ");
     else printf("Unknown ");
     fflush(stdout);
   }

   insize=file_size(infile);

  /* decompress the file */
   if (quality>=0 && !retry) {
     jpeg_start_decompress(&dinfo);

     buf = malloc(sizeof(JSAMPROW)*dinfo.output_height);
     if (!buf) no_memory();
     for (j=0;j<dinfo.output_height;j++) {
       buf[j]=malloc(sizeof(JSAMPLE)*dinfo.output_width*
		     dinfo.out_color_components);
       if (!buf[j]) no_memory();
     }

     while (dinfo.output_scanline < dinfo.output_height) {
       jpeg_read_scanlines(&dinfo,&buf[dinfo.output_scanline],
			   dinfo.output_height-dinfo.output_scanline);
     }
   } else {
     coef_arrays = jpeg_read_coefficients(&dinfo);
   }

   if (!retry) {
     if (!global_error_counter) printf(" [OK] ");
     else {
	   printf(" [WARNING] defective file, skipped.\n");
       jpeg_abort_decompress(&dinfo);
       fclose(infile);
       for (j=0;j<dinfo.output_height;j++) free(buf[j]);
       free(buf); buf=NULL;
       outfname=NULL;
       continue;
     }
       
     fflush(stdout);
   }

   outfname=(noaction ? name2 : name1);
 
   if (dest && !noaction) {
     strcpy(newname,dest_path);
#ifndef AMIGA
     strcat(newname,"/"); 
#endif
	 strcat(newname,(char*)basename(argv[i]));
     if (file_exists(newname)) {
       fprintf(stderr,"jpegoptim: target file already exists: %s\n",
	       newname);
       jpeg_abort_decompress(&dinfo);
       fclose(infile);
       for (j=0;j<dinfo.output_height;j++) free(buf[j]);
       free(buf); buf=NULL;
       outfname=NULL;
       continue;
     }
     outfname=newname;
   }

   if ((outfile=fopen(outfname,"w"))==NULL) {
     if (!quiet_mode) fprintf(stderr,"\njpegoptim: error creating "
			      "file: %s\n", outfname);
     exit(1);
   }


   if (setjmp(jcerr.setjmp_buffer)) {
      jpeg_abort_compress(&cinfo);
      jpeg_abort_decompress(&dinfo);
      fclose(outfile);
      if (infile) fclose(infile);
      printf(" [Compress ERROR]\n");
      for (j=0;j<dinfo.output_height;j++) free(buf[j]);
      free(buf); buf=NULL;
      if (file_exists(outfname)) delete_file(outfname);
      outfname=NULL;
      continue;
   }

   jpeg_stdio_dest(&cinfo, outfile);

   if (quality>=0 && !retry) {
     cinfo.in_color_space=dinfo.out_color_space;
     cinfo.input_components=dinfo.output_components;
     cinfo.image_width=dinfo.image_width;
     cinfo.image_height=dinfo.image_height;
     jpeg_set_defaults(&cinfo); 
     jpeg_set_quality(&cinfo,quality,TRUE);
     cinfo.optimize_coding = TRUE;
     
     j=0;
     jpeg_start_compress(&cinfo,TRUE);
     while (cinfo.next_scanline < cinfo.image_height) {
       jpeg_write_scanlines(&cinfo,&buf[cinfo.next_scanline],
			    dinfo.output_height);
     }
     
     for (j=0;j<dinfo.output_height;j++) free(buf[j]);
     free(buf); buf=NULL;
   } else {
     jpeg_copy_critical_parameters(&dinfo, &cinfo);
     cinfo.optimize_coding = TRUE;
     jpeg_write_coefficients(&cinfo, coef_arrays);
   }

   jpeg_finish_compress(&cinfo);
   jpeg_finish_decompress(&dinfo);
   fclose(infile);
   outsize=file_size(outfile);
   fclose(outfile);

   if (quality>=0 && outsize>=insize && !retry) {
     if (verbose_mode) printf("(retry w/lossless) ");
     retry=1;
     goto retry_point; 
   }

   retry=0;
   ratio=(insize-outsize)*100.0/insize;
   printf("%d --> %d bytes (%0.2lf%%), ",insize,outsize,ratio);
   average_count++;
   average_rate+=(ratio<0 ? 0.0 : ratio);

   if (outsize<insize || force) {
        total_save+=(insize-outsize)/1024.0;
	printf("optimized.\n");
        if (noaction || dest) { continue; }
	if (!delete_file(argv[i])) {
		if (verbose_mode&&!quiet_mode) 
		  fprintf(stderr,"Renaming: %s to %s\n",outfname,argv[i]);
#ifdef AMIGA
		if (crap_rename(outfname,argv[i])) {
#else
        if (rename(outfname,argv[i])) {
#endif
		  fprintf(stderr,"Cannot rename temp file.\n");
	          exit(3);
		}
	}
	
   } else {
	printf("skipped.\n");
	if (!noaction) delete_file(outfname);
   }

  } while (++i<argc);

  if (noaction && file_exists(outfname)) delete_file(outfname);
  if (totals_mode&&!quiet_mode)
    printf("Average ""compression"" (%ld files): %0.2lf%% (%0.0lfk)\n",
	   average_count, average_rate/average_count, total_save);

  jpeg_destroy_decompress(&dinfo);
  jpeg_destroy_compress(&cinfo);


  return 0;
}

/* :-) */
