/*******************************************************************
* JPEGoptim
* Copyright (c) Timo Kokkonen, 1996-2023.
* All Rights Reserved.
*
* requires libjpeg (Independent JPEG Group's JPEG software
* release 6a or later...)
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
* This file is part of JPEGoptim.
*
* JPEGoptim 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 3 of the License, or
* (at your option) any later version.
*
* JPEGoptim 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 JPEGoptim. If not, see .
*/
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include
#include
#ifdef HAVE_UNISTD_H
#include
#endif
#if HAVE_DIRENT_H
#include
#endif
#if HAVE_FCNTL_H
#include
#endif
#if HAVE_SYS_STAT_H
#include
#endif
#if HAVE_SYS_TYPES_H
#include
#endif
#if HAVE_SYS_WAIT_H
#include
#endif
#if HAVE_GETOPT_H && HAVE_GETOPT_LONG
#include
#else
#include "getopt.h"
#endif
#include
#include
#include
#include
#include
#include
#include
#include "jpegmarker.h"
#include "jpegoptim.h"
#define VERSION "1.5.5"
#define COPYRIGHT "Copyright (C) 1996-2023, Timo Kokkonen"
#if HAVE_WAIT && HAVE_FORK
#define PARALLEL_PROCESSING 1
#define MAX_WORKERS 256
#endif
#define FREE_LINE_BUF(buf,lines) { \
int j; \
for (j=0;jerr;
(*cinfo->err->output_message)(cinfo);
if (myerr->jump_set)
longjmp(myerr->setjmp_buffer, 1);
else
fatal("fatal error");
}
METHODDEF(void) my_output_message (j_common_ptr cinfo)
{
char buffer[JMSG_LENGTH_MAX+1];
(*cinfo->err->format_message)((j_common_ptr)cinfo, buffer);
buffer[sizeof(buffer) - 1] = 0;
if (verbose_mode)
fprintf(jpeg_log_fh, " (%s) ", buffer);
global_error_counter++;
strncopy(last_error, buffer, sizeof(last_error));
}
void print_usage(void)
{
fprintf(stderr,PROGRAMNAME " v" VERSION " " COPYRIGHT "\n");
fprintf(stderr,
"Usage: " PROGRAMNAME " [options] \n\n"
" -d, --dest=\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, --max=\n"
" set maximum image quality factor (disables lossless\n"
" optimization mode, which is by default on)\n"
" Valid quality values: 0 - 100\n"
" -n, --noaction don't really optimize files, just print results\n"
" -S, --size=\n"
" Try to optimize file to given size (disables lossless\n"
" optimization mode). Target size is specified either in\n"
" kilo bytes (1 - n) or as percentage (1%% - 99%%)\n"
" -T, --threshold=\n"
" keep old file if the gain is below a threshold (%%)\n"
#ifdef PARALLEL_PROCESSING
" -w, --workers=\n"
" set maximum number of parallel threads (default is 1)\n"
#endif
" -b, --csv print progress info in CSV format\n"
" -o, --overwrite overwrite target file even if it exists (meaningful\n"
" only when used with -d, --dest option)\n"
" -p, --preserve preserve file timestamps\n"
" -P, --preserve-perms\n"
" preserve original file permissions by overwriting it\n"
" -q, --quiet quiet mode\n"
" -t, --totals print totals after processing all files\n"
" -v, --verbose enable verbose mode (positively chatty)\n"
" -V, --version print program version\n\n"
" -s, --strip-all strip all markers from output file\n"
" --strip-none do not strip any markers\n"
" --strip-adobe strip Adobe (APP14) markers from output file\n"
" --strip-com strip Comment markers from output file\n"
" --strip-exif strip Exif markers from output file\n"
" --strip-iptc strip IPTC/Photoshop (APP13) markers from output file\n"
" --strip-icc strip ICC profile markers from output file\n"
" --strip-jfif strip JFIF markers from output file\n"
" --strip-jfxx strip JFXX (JFIF Extension) markers from output file\n"
" --strip-xmp strip XMP markers markers from output file\n"
"\n"
" --keep-all do not strip any markers (same as --strip-none)\n"
" --keep-adobe preserve any Adobe (APP14) markers\n"
" --keep-com preserve any Comment markers\n"
" --keep-exif preserve any Exif markers\n"
" --keep-iptc preserve any IPTC/Photoshop (APP13) markers\n"
" --keep-icc preserve any ICC profile markers\n"
" --keep-jfif preserve any JFIF markers\n"
" --keep-jfxx preserve any JFXX (JFIF Extension) markers\n"
" --keep-xmp preserve any XMP markers markers\n"
"\n"
" --all-normal force all output files to be non-progressive\n"
" --all-progressive force all output files to be progressive\n"
#ifdef HAVE_ARITH_CODE
" --all-arith force all output files to use arithmetic coding\n"
" --all-huffman force all output files to use Huffman coding\n"
#endif
" --stdout send output to standard output (instead of a file)\n"
" --stdin read input from standard input (instead of a file)\n"
" --files-stdin Read names of files to process from stdin\n"
" --files-from=FILE Read names of files to process from a file\n"
" --nofix skip processing of input files if they contain any errors\n"
"\n\n");
}
void print_version()
{
struct jpeg_error_mgr jerr;
#ifdef __DATE__
printf(PROGRAMNAME " v%s %s (%s)\n",VERSION,HOST_TYPE,__DATE__);
#else
printf(PROGRAMNAME " v%s %s\n",VERSION,HOST_TYPE);
#endif
printf(COPYRIGHT "\n\n");
printf("This program comes with ABSOLUTELY NO WARRANTY. This is free software,\n"
"and you are welcome to redistribute it under certain conditions.\n"
"See the GNU General Public License for more details.\n\n");
if (!jpeg_std_error(&jerr))
fatal("jpeg_std_error() failed");
printf("\nlibjpeg version: %s\n%s\n",
jerr.jpeg_message_table[JMSG_VERSION],
jerr.jpeg_message_table[JMSG_COPYRIGHT]);
}
void parse_arguments(int argc, char **argv, char *dest_path, size_t dest_path_len)
{
int opt_index;
int i, c;
while(1) {
opt_index=0;
if ((c = getopt_long(argc,argv,"d:hm:nstqvfVpPoT:S:bw:",
long_options, &opt_index)) == -1)
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
fatal("invalid argument for -m, --max");
}
break;
case 'd':
if (realpath(optarg,dest_path)==NULL)
fatal("invalid destination directory: %s", optarg);
if (!is_directory(dest_path))
fatal("destination not a directory: %s", dest_path);
strncatenate(dest_path, DIR_SEPARATOR_S, dest_path_len);
if (verbose_mode)
fprintf(stderr,"Destination directory: %s\n",dest_path);
dest=1;
break;
case 'v':
verbose_mode++;
break;
case 'h':
print_usage();
exit(0);
break;
case 'q':
quiet_mode=1;
break;
case 't':
totals_mode=1;
break;
case 'n':
noaction=1;
break;
case 'f':
force=1;
break;
case 'b':
csv=1;
quiet_mode=1;
break;
case 'V':
print_version();
exit(0);
break;
case 'o':
overwrite_mode=1;
break;
case 'p':
preserve_mode=1;
break;
case 'P':
preserve_perms=1;
break;
case 's':
save_exif=0;
save_iptc=0;
save_com=0;
save_icc=0;
save_xmp=0;
save_adobe=0;
save_jfif=0;
save_jfxx=0;
break;
case 'T':
{
double tmpvar;
if (sscanf(optarg,"%lf", &tmpvar) == 1) {
threshold=tmpvar;
if (threshold < 0) threshold=0;
if (threshold > 100) threshold=100;
}
else fatal("invalid argument for -T, --threshold");
}
break;
case 'S':
{
unsigned int tmpvar;
if (sscanf(optarg,"%u", &tmpvar) == 1) {
if (tmpvar > 0 && tmpvar < 100 &&
optarg[strlen(optarg)-1] == '%' ) {
target_size=-tmpvar;
} else {
target_size=tmpvar;
}
quality=100;
}
else fatal("invalid argument for -S, --size");
}
break;
#ifdef PARALLEL_PROCESSING
case 'w':
{
int tmpvar;
if (sscanf(optarg, "%d", &tmpvar) == 1) {
if (tmpvar > 0 && tmpvar <= MAX_WORKERS)
max_workers = tmpvar;
}
else fatal("invalid argument for -w, --workers");
}
break;
#endif
case 'F':
{
if (optarg[0] == '-' && optarg[1] == 0) {
files_stdin = 1;
break;
}
if (!is_file(optarg, NULL))
fatal("argument for --files-from must be a file");
if ((files_from = fopen(optarg, "r")) == NULL)
fatal("cannot open file: '%s'", optarg);
}
break;
case '?':
exit(1);
}
}
/* check for '-' option indicating input is from stdin... */
i = optind;
while (argv[i]) {
if (argv[i][0]=='-' && argv[i][1]==0)
stdin_mode=1;
i++;
}
if (stdin_mode)
stdout_mode=1;
if (all_normal && all_progressive)
fatal("cannot specify both --all-normal and --all-progressive");
if (files_stdin)
files_from = stdin;
if (stdin_mode && files_from == stdin)
fatal("cannot specify both --stdin and --files-stdin");
}
void own_signal_handler(int a)
{
if (verbose_mode > 1)
fprintf(stderr,PROGRAMNAME ": died from signal: %d\n",a);
exit(2);
}
void write_markers(struct jpeg_decompress_struct *dinfo,
struct jpeg_compress_struct *cinfo)
{
jpeg_saved_marker_ptr mrk;
int write_marker;
const char *s_name;
if (!cinfo || !dinfo)
fatal("invalid call to write_markers()");
mrk=dinfo->marker_list;
while (mrk) {
write_marker = 0;
s_name = jpeg_special_marker_name(mrk);
/* Check for markers to save... */
if (save_com && mrk->marker == JPEG_COM)
write_marker++;
if (save_iptc && !strncmp(s_name, "IPTC", 5))
write_marker++;
if (save_exif && !strncmp(s_name, "Exif", 5))
write_marker++;
if (save_icc && !strncmp(s_name, "ICC", 4))
write_marker++;
if (save_xmp && !strncmp(s_name, "XMP", 4))
write_marker++;
if (save_jfxx && !strncmp(s_name, "JFXX", 5))
write_marker++;
if (save_adobe && !strncmp(s_name, "Adobe", 6))
write_marker++;
if (strip_none)
write_marker++;
/* libjpeg emits some markers automatically so skip these to avoid duplicates... */
if (!strncmp(s_name, "JFIF", 5))
write_marker=0;
if (verbose_mode > 2)
fprintf(jpeg_log_fh, " (Marker %s [%s]: %s) ", jpeg_marker_name(mrk->marker),
s_name, (write_marker ? "Keep" : "Discard"));
if (write_marker)
jpeg_write_marker(cinfo, mrk->marker, mrk->data, mrk->data_length);
mrk=mrk->next;
}
}
unsigned int parse_markers(const struct jpeg_decompress_struct *dinfo,
char *str, unsigned int str_size, unsigned int *markers_total_size)
{
jpeg_saved_marker_ptr m;
unsigned int count = 0;
char *seen;
size_t marker_types = jpeg_special_marker_types_count();
int com_seen = 0;
int special;
if ((seen = malloc(marker_types)) == NULL)
fatal("not enough of memory");
memset(seen, 0, marker_types);
str[0] = 0;
*markers_total_size = 0;
m = dinfo->marker_list;
while (m) {
count++;
*markers_total_size += m->data_length;
if ((special = jpeg_special_marker(m)) >= 0) {
if (!seen[special])
str_add_list(str, str_size, jpeg_special_marker_types[special].name, ",");
seen[special]++;
}
if (m->marker == JPEG_COM && !com_seen) {
str_add_list(str, str_size, "COM", ",");
com_seen = 1;
}
m = m->next;
}
free(seen);
return count;
}
int optimize(FILE *log_fh, const char *filename, const char *newname,
const char *tmpdir, struct stat *file_stat,
double *rate, double *saved)
{
FILE *infile = NULL;
FILE *outfile = NULL;
const char *outfname = NULL;
char tmpfilename[MAXPATHLEN];
struct jpeg_decompress_struct dinfo;
struct jpeg_compress_struct cinfo;
struct my_error_mgr jcerr, jderr;
JSAMPARRAY buf = NULL;
unsigned char *outbuffer = NULL;
size_t outbuffersize = 0;
unsigned char *inbuffer = NULL;
size_t inbuffersize = 0;
size_t inbufferused = 0;
jvirt_barray_ptr *coef_arrays = NULL;
char marker_str[256];
unsigned int marker_in_count, marker_in_size;
long insize = 0, outsize = 0, lastsize = 0;
long inpos;
int j;
int oldquality, searchcount, searchdone;
double ratio;
int res = -1;
jpeg_log_fh = log_fh;
/* 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;
jderr.jump_set = 0;
/* 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;
jcerr.jump_set = 0;
if (rate)
*rate = 0.0;
if (saved)
*saved = 0.0;
retry_point:
if (filename) {
if ((infile = fopen(filename, "rb")) == NULL) {
warn("cannot open file: %s", filename);
res = 1;
goto exit_point;
}
} else {
infile = stdin;
set_filemode_binary(infile);
}
if (setjmp(jderr.setjmp_buffer)) {
/* Error handler for decompress */
abort_decompress:
jpeg_abort_decompress(&dinfo);
fclose(infile);
if (buf)
FREE_LINE_BUF(buf,dinfo.output_height);
if (!quiet_mode || csv)
fprintf(log_fh,csv ? ",,,,,error\n" : " [ERROR]\n");
jderr.jump_set=0;
res = 1;
goto exit_point;
} else {
jderr.jump_set=1;
}
if (!retry && (!quiet_mode || csv)) {
fprintf(log_fh,csv ? "%s," : "%s ",(filename ? filename:"stdin"));
fflush(log_fh);
}
/* Prepare to decompress */
if (stdin_mode || stdout_mode) {
if (inbuffer)
free(inbuffer);
inbuffersize=65536;
inbuffer=malloc(inbuffersize);
if (!inbuffer)
fatal("not enough memory");
}
global_error_counter=0;
jpeg_save_markers(&dinfo, JPEG_COM, 0xffff);
for (j=0;j<=15;j++) {
jpeg_save_markers(&dinfo, JPEG_APP0+j, 0xffff);
}
jpeg_custom_src(&dinfo, infile, &inbuffer, &inbuffersize, &inbufferused, 65536);
jpeg_read_header(&dinfo, TRUE);
/* Check for known (Exif, IPTC, ICC , XMP, ...) markers */
marker_in_count = parse_markers(&dinfo, marker_str, sizeof(marker_str),
&marker_in_size);
if (verbose_mode > 1) {
fprintf(log_fh,"%d markers found in input file (total size %d bytes)\n",
marker_in_count,marker_in_size);
fprintf(log_fh,"coding: %s\n", (dinfo.arith_code == TRUE ? "Arithmetic" : "Huffman"));
}
if (!retry && (!quiet_mode || csv)) {
fprintf(log_fh,csv ? "%dx%d,%dbit,%c," : "%dx%d %dbit %c ",(int)dinfo.image_width,
(int)dinfo.image_height,(int)dinfo.num_components*8,
(dinfo.progressive_mode?'P':'N'));
if (!csv)
fprintf(log_fh,"%s",marker_str);
fflush(log_fh);
}
if ((insize=file_size(infile)) < 0)
fatal("failed to stat() input file");
/* Decompress the image */
if (quality >= 0 && !retry) {
jpeg_start_decompress(&dinfo);
/* Allocate line buffer to store the decompressed image */
buf = malloc(sizeof(JSAMPROW)*dinfo.output_height);
if (!buf) fatal("not enough memory");
for (j=0;j 0 && inpos < insize) {
if (!quiet_mode)
fprintf(log_fh, " (%lu bytes extraneous data found after end of image) ",
insize - inpos);
if (nofix_mode)
global_error_counter++;
}
if (!retry && !quiet_mode) {
fprintf(log_fh,(global_error_counter==0 ? " [OK] " : " [WARNING] "));
fflush(log_fh);
}
if (stdin_mode)
insize = inbufferused;
if (nofix_mode && global_error_counter != 0) {
/* Skip files containing any errors (or warnings) */
goto abort_decompress;
}
if (dest && !noaction) {
if (file_exists(newname) && !overwrite_mode) {
if (!quiet_mode)
fprintf(log_fh, " (target file already exists) ");
goto abort_decompress;
}
}
if (setjmp(jcerr.setjmp_buffer)) {
/* Error handler for compress failures */
jpeg_abort_compress(&cinfo);
jpeg_abort_decompress(&dinfo);
fclose(infile);
if (!quiet_mode)
fprintf(log_fh," [Compress ERROR: %s]\n",last_error);
if (buf)
FREE_LINE_BUF(buf,dinfo.output_height);
jcerr.jump_set=0;
res = 2;
goto exit_point;
} else {
jcerr.jump_set=1;
}
lastsize = 0;
searchcount = 0;
searchdone = 0;
oldquality = 200;
if (target_size != 0) {
/* Always start with quality 100 if -S option specified... */
quality = 100;
}
binary_search_loop:
/* Allocate memory buffer that should be large enough to store the output JPEG... */
if (outbuffer)
free(outbuffer);
outbuffersize=insize + 32768;
outbuffer=malloc(outbuffersize);
if (!outbuffer)
fatal("not enough memory");
/* setup custom "destination manager" for libjpeg to write to our buffer */
jpeg_memory_dest(&cinfo, &outbuffer, &outbuffersize, 65536);
if (quality >= 0 && !retry) {
/* Lossy "optimization" ... */
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);
#ifdef HAVE_JINT_DC_SCAN_OPT_MODE
if (jpeg_c_int_param_supported(&cinfo, JINT_DC_SCAN_OPT_MODE))
jpeg_c_set_int_param(&cinfo, JINT_DC_SCAN_OPT_MODE, 1);
#endif
if (all_normal || (!dinfo.progressive_mode && !all_progressive)) {
/* Explicitly disable progressive mode. */
cinfo.scan_info = NULL;
cinfo.num_scans = 0;
} else if (all_progressive || dinfo.progressive_mode) {
/* Enable progressive mode. */
jpeg_simple_progression(&cinfo);
}
cinfo.optimize_coding = TRUE;
#ifdef HAVE_ARITH_CODE
if (arith_mode >= 0)
cinfo.arith_code = (arith_mode > 0 ? TRUE : FALSE);
#endif
if (dinfo.saw_JFIF_marker && (save_jfif || strip_none)) {
cinfo.write_JFIF_header = TRUE;
} else {
cinfo.write_JFIF_header = FALSE;
}
if (dinfo.saw_Adobe_marker && (save_adobe || strip_none)) {
/* If outputting Adobe marker, don't write JFIF marker... */
cinfo.write_JFIF_header = FALSE;
}
j=0;
jpeg_start_compress(&cinfo,TRUE);
/* Write markers */
write_markers(&dinfo,&cinfo);
/* Write image */
while (cinfo.next_scanline < cinfo.image_height) {
jpeg_write_scanlines(&cinfo,&buf[cinfo.next_scanline],
dinfo.output_height);
}
} else {
/* Lossless optimization ... */
jpeg_copy_critical_parameters(&dinfo, &cinfo);
#ifdef HAVE_JINT_DC_SCAN_OPT_MODE
if (jpeg_c_int_param_supported(&cinfo, JINT_DC_SCAN_OPT_MODE))
jpeg_c_set_int_param(&cinfo, JINT_DC_SCAN_OPT_MODE, 1);
#endif
if (all_normal || (!dinfo.progressive_mode && !all_progressive)) {
/* Explicitly disable progressive mode. */
cinfo.scan_info = NULL;
cinfo.num_scans = 0;
} else if (all_progressive || dinfo.progressive_mode) {
/* Enable progressive mode. */
jpeg_simple_progression(&cinfo);
}
cinfo.optimize_coding = TRUE;
#ifdef HAVE_ARITH_CODE
if (arith_mode >= 0)
cinfo.arith_code = (arith_mode > 0 ? TRUE : FALSE);
#endif
if (dinfo.saw_JFIF_marker && (save_jfif || strip_none)) {
cinfo.write_JFIF_header = TRUE;
} else {
cinfo.write_JFIF_header = FALSE;
}
if (dinfo.saw_Adobe_marker && (save_adobe || strip_none)) {
/* If outputting Adobe marker, don't write JFIF marker... */
cinfo.write_JFIF_header = FALSE;
}
/* Write image */
jpeg_write_coefficients(&cinfo, coef_arrays);
/* Write markers */
write_markers(&dinfo,&cinfo);
}
jpeg_finish_compress(&cinfo);
outsize=outbuffersize;
if (target_size != 0 && !retry) {
/* Perform (binary) search to try to reach target file size... */
long osize = outsize/1024;
long isize = insize/1024;
long tsize = target_size;
if (tsize < 0) {
tsize=((-target_size)*insize/100)/1024;
if (tsize < 1)
tsize = 1;
}
if (osize == tsize || searchdone || searchcount >= 8 || tsize > isize) {
if (searchdone < 42 && lastsize > 0) {
if (labs(osize-tsize) > labs(lastsize-tsize)) {
if (verbose_mode) fprintf(log_fh,"(revert to %d)",oldquality);
searchdone=42;
quality=oldquality;
goto binary_search_loop;
}
}
if (verbose_mode)
fprintf(log_fh," ");
} else {
int newquality;
int dif = floor((abs(oldquality-quality)/2.0)+0.5);
if (osize > tsize) {
newquality=quality-dif;
if (dif < 1) { newquality--; searchdone=1; }
if (newquality < 0) { newquality=0; searchdone=2; }
} else {
newquality=quality+dif;
if (dif < 1) { newquality++; searchdone=3; }
if (newquality > 100) { newquality=100; searchdone=4; }
}
oldquality=quality;
quality=newquality;
if (verbose_mode)
fprintf(log_fh,"(try %d)",quality);
lastsize=osize;
searchcount++;
goto binary_search_loop;
}
}
if (buf)
FREE_LINE_BUF(buf,dinfo.output_height);
jpeg_finish_decompress(&dinfo);
fclose(infile);
if (quality >= 0 && outsize >= insize && !retry && !stdin_mode) {
if (verbose_mode)
fprintf(log_fh,"(retry w/lossless) ");
retry=1;
goto retry_point;
}
retry=0;
ratio=(insize-outsize)*100.0/insize;
if (!quiet_mode || csv)
fprintf(log_fh,csv ? "%ld,%ld,%0.2f," : "%ld --> %ld bytes (%0.2f%%), ",insize,outsize,ratio);
if (rate) {
*rate = (ratio < 0 ? 0.0 : ratio);
}
if ((outsize < insize && ratio >= threshold) || force) {
if (saved) {
*saved = (insize - outsize) / 1024.0;
}
if (!quiet_mode || csv)
fprintf(log_fh,csv ? "optimized\n" : "optimized.\n");
if (noaction) {
res = 0;
goto exit_point;
}
if (stdout_mode) {
outfname=NULL;
set_filemode_binary(stdout);
if (fwrite(outbuffer,outbuffersize,1,stdout) != 1)
fatal("%s, write failed to stdout",(stdin_mode ? "stdin" : filename));
} else {
if (preserve_perms && !dest) {
/* make backup of the original file */
int newlen = snprintf(tmpfilename, sizeof(tmpfilename),
"%s.jpegoptim.bak", newname);
if (newlen >= sizeof(tmpfilename))
warn("temp filename too long: %s", tmpfilename);
if (verbose_mode > 1 && !quiet_mode)
fprintf(log_fh,"%s, creating backup as: %s\n",
(stdin_mode ? "stdin" : filename), tmpfilename);
if (file_exists(tmpfilename))
fatal("%s, backup file already exists: %s",
(stdin_mode ?" stdin" : filename), tmpfilename);
if (copy_file(newname,tmpfilename))
fatal("%s, failed to create backup: %s",
(stdin_mode ? "stdin" : filename), tmpfilename);
if ((outfile=fopen(newname,"wb"))==NULL)
fatal("%s, error opening output file: %s",
(stdin_mode ? "stdin" : filename), newname);
outfname=newname;
} else {
#ifdef HAVE_MKSTEMPS
/* rely on mkstemps() to create us temporary file safely... */
int newlen = snprintf(tmpfilename,sizeof(tmpfilename),
"%sjpegoptim-%d-%d.XXXXXX.tmp",
tmpdir, (int)getuid(), (int)getpid());
if (newlen >= sizeof(tmpfilename))
warn("temp filename too long: %s", tmpfilename);
int tmpfd = mkstemps(tmpfilename,4);
if (tmpfd < 0)
fatal("%s, error creating temp file %s: mkstemps() failed",
(stdin_mode ? "stdin" : filename), tmpfilename);
if ((outfile = fdopen(tmpfd,"wb")) == NULL)
#else
/* if platform is missing mkstemps(), try to create
at least somewhat "safe" temp file... */
snprintf(tmpfilename,sizeof(tmpfilename),
"%sjpegoptim-%d-%d.%ld.tmp", tmpdir,
(int)getuid(), (int)getpid(), (long)time(NULL));
if ((outfile = fopen(tmpfilename,"wb")) == NULL)
#endif
fatal("error opening temporary file: %s", tmpfilename);
outfname=tmpfilename;
}
if (verbose_mode > 1 && !quiet_mode)
fprintf(log_fh,"writing %lu bytes to file: %s\n",
(long unsigned int)outbuffersize, outfname);
if (fwrite(outbuffer,outbuffersize,1,outfile) != 1)
fatal("write failed to file: %s", outfname);
fclose(outfile);
}
if (outfname) {
if (preserve_mode) {
/* preserve file modification time */
if (verbose_mode > 1 && !quiet_mode)
fprintf(log_fh,"set file modification time same as in original: %s\n",
outfname);
#if defined(HAVE_UTIMENSAT) && defined(HAVE_STRUCT_STAT_ST_MTIM)
struct timespec time_save[2];
time_save[0].tv_sec = 0;
time_save[0].tv_nsec = UTIME_OMIT; /* omit atime */
time_save[1] = file_stat->st_mtim;
if (utimensat(AT_FDCWD,outfname,time_save,0) != 0)
warn("failed to reset output file time/date");
#else
struct utimbuf time_save;
time_save.actime=file_stat->st_atime;
time_save.modtime=file_stat->st_mtime;
if (utime(outfname,&time_save) != 0)
warn("failed to reset output file time/date");
#endif
}
if (preserve_perms && !dest) {
/* original file was already replaced, remove backup... */
if (verbose_mode > 1 && !quiet_mode)
fprintf(log_fh,"removing backup file: %s\n", tmpfilename);
if (delete_file(tmpfilename))
warn("failed to remove backup file: %s", tmpfilename);
} else {
/* make temp file to be the original file... */
/* preserve file mode */
if (chmod(outfname,(file_stat->st_mode & 0777)) != 0)
warn("failed to set output file mode");
/* preserve file group (and owner if run by root) */
if (chown(outfname,
(geteuid()==0 ? file_stat->st_uid : -1),
file_stat->st_gid) != 0)
warn("failed to reset output file group/owner");
if (verbose_mode > 1 && !quiet_mode)
fprintf(log_fh,"renaming: %s to %s\n", outfname, newname);
if (rename_file(outfname, newname))
fatal("cannot rename temp file");
}
}
} else {
if (!quiet_mode || csv)
fprintf(log_fh,csv ? "skipped\n" : "skipped.\n");
if (stdout_mode) {
set_filemode_binary(stdout);
if (fwrite(inbuffer, inbufferused, 1, stdout) != 1)
fatal("%s, write failed to stdout",
(stdin_mode ? "stdin" : filename));
}
}
res = 0;
exit_point:
if (inbuffer)
free(inbuffer);
if (outbuffer)
free(outbuffer);
jpeg_destroy_compress(&cinfo);
jpeg_destroy_decompress(&dinfo);
return res;
}
#ifdef PARALLEL_PROCESSING
int wait_for_worker(FILE *log_fh)
{
FILE *p;
struct worker *w;
char buf[1024];
int wstatus;
pid_t pid;
int j, e;
int state = 0;
double val;
double rate = 0.0;
double saved = 0.0;
if ((pid = wait(&wstatus)) < 0)
return pid;
w = NULL;
for (j = 0; j < MAX_WORKERS; j++) {
if (workers[j].pid == pid) {
w = &workers[j];
break;
}
}
if (!w)
fatal("Unknown worker[%d] process found\n", pid);
if (WIFEXITED(wstatus)) {
e = WEXITSTATUS(wstatus);
if (verbose_mode)
fprintf(log_fh, "worker[%d] [slot=%d] exited: %d\n",
pid, j, e);
if (e == 0) {
//average_count++;
//average_rate += rate;
//total_save += saved;
} else if (e == 1) {
decompress_err_count++;
} else if (e == 2) {
compress_err_count++;
}
} else {
fatal("worker[%d] killed", pid);
}
p = fdopen(w->read_pipe, "r");
if (!p) fatal("fdopen failed()");
while (fgets(buf, sizeof(buf), p)) {
if (verbose_mode > 2)
fprintf(log_fh, "worker[%d] PIPE: %s", pid, buf);
if (state == 0 && buf[0] == '\n') {
state=1;
continue;
}
if (state == 1 && !strncmp(buf, "STAT", 4)) {
state=2;
continue;
}
if (state >= 2) {
if (sscanf(buf, "%lf", &val) == 1) {
if (state == 2) {
rate = val;
}
else if (state == 3) {
saved = val;
average_count++;
average_rate += rate;
total_save += saved;
}
}
state++;
continue;
}
if (state == 0)
fprintf(log_fh, "%s", buf);
}
close(w->read_pipe);
w->pid = -1;
w->read_pipe = -1;
worker_count --;
return pid;
}
#endif
/****************************************************************************/
int main(int argc, char **argv)
{
struct stat file_stat;
char tmpfilename[MAXPATHLEN + 1],tmpdir[MAXPATHLEN + 1];
char newname[MAXPATHLEN + 1], dest_path[MAXPATHLEN + 1];
char namebuf[MAXPATHLEN + 2];
const char *filename;
volatile int i;
int res;
double rate, saved;
FILE *log_fh;
#ifdef PARALLEL_PROCESSING
struct worker *w;
int pipe_fd[2];
pid_t pid;
int j;
/* Allocate table to keep track of child processes... */
if (!(workers = malloc(sizeof(struct worker) * MAX_WORKERS)))
fatal("not enough memory");
for (i = 0; i < MAX_WORKERS; i++) {
workers[i].pid = -1;
workers[i].read_pipe = -1;
}
#endif
umask(077);
signal(SIGINT,own_signal_handler);
signal(SIGTERM,own_signal_handler);
/* Parse command line parameters */
parse_arguments(argc, argv, dest_path, sizeof(dest_path));
log_fh = (stdout_mode ? stderr : stdout);
if (verbose_mode) {
if (quality >= 0 && target_size == 0)
fprintf(log_fh, "Image quality limit set to: %d\n", quality);
if (threshold >= 0)
fprintf(log_fh, "Compression threshold (%%) set to: %0.1lf\n", threshold);
if (all_normal)
fprintf(log_fh, "All output files will be non-progressive\n");
if (all_progressive)
fprintf(log_fh, "All output files will be progressive\n");
if (target_size > 0)
fprintf(log_fh, "Target size for output files set to: %d Kbytes.\n",
target_size);
if (target_size < 0)
fprintf(log_fh, "Target size for output files set to: %d%%\n",
-target_size);
#ifdef PARALLEL_PROCESSING
if (max_workers > 0)
fprintf(log_fh, "Using maximum of %d parallel threads\n", max_workers);
#endif
}
if (stdin_mode) {
/* Process just one file, if source is stdin... */
res = optimize(stderr, NULL, NULL, NULL, &file_stat, NULL, NULL);
return (res == 0 ? 0 : 1);
}
i=(optind > 0 ? optind : 1);
if (files_from == NULL && argc <= i) {
if (!quiet_mode)
fprintf(stderr, PROGRAMNAME ": file argument(s) missing\n"
"Try '" PROGRAMNAME " --help' for more information.\n");
exit(1);
}
/* Main loop to process input files */
do {
if (files_from) {
if (!fgetstr(namebuf, sizeof(namebuf), files_from))
break;
filename = namebuf;
} else {
filename = argv[i];
}
if (*filename == 0)
continue;
if (verbose_mode > 1)
fprintf(log_fh, "processing file: %s\n", filename);
if (strnlen(filename, MAXPATHLEN + 1) > MAXPATHLEN) {
warn("skipping too long filename: %s", filename);
continue;
}
if (!noaction) {
/* generate tmp dir & new filename */
if (dest) {
strncopy(tmpdir, dest_path, sizeof(tmpdir));
strncopy(newname, dest_path, sizeof(newname));
if (!splitname(filename, tmpfilename, sizeof(tmpfilename)))
fatal("splitname() failed for: %s", filename);
strncatenate(newname, tmpfilename, sizeof(newname));
} else {
if (!splitdir(filename, tmpdir, sizeof(tmpdir)))
fatal("splitdir() failed for: %s", filename);
strncopy(newname, filename, sizeof(newname));
}
}
if (file_exists(filename)) {
if (!is_file(filename, &file_stat)) {
if (is_directory(filename))
warn("skipping directory: %s", filename);
else
warn("skipping special file: %s", filename);
continue;
}
} else {
warn("file not found: %s", filename);
continue;
}
#ifdef PARALLEL_PROCESSING
if (max_workers > 1) {
/* Multi process mode, run up to max_workers processes simultaneously... */
if (worker_count >= max_workers) {
// wait for a worker to exit...
wait_for_worker(log_fh);
}
if (pipe(pipe_fd) < 0)
fatal("failed to open pipe");
pid = fork();
if (pid < 0)
fatal("fork() failed");
if (pid == 0) {
/* Child process starts here... */
if (files_from)
fclose(files_from);
close(pipe_fd[0]);
FILE *p;
if (!(p = fdopen(pipe_fd[1],"w")))
fatal("worker: fdopen failed");
res = optimize(p, filename, newname, tmpdir, &file_stat, &rate, &saved);
if (res == 0)
fprintf(p, "\n\nSTATS\n%lf\n%lf\n", rate, saved);
exit(res);
} else {
/* Parent continues here... */
close(pipe_fd[1]);
w = NULL;
for (j = 0; j < MAX_WORKERS; j++) {
if (workers[j].pid < 0) {
w = &workers[j];
break;
}
}
if (!w)
fatal("no space to start a new worker (%d)", worker_count);
w->pid = pid;
w->read_pipe = pipe_fd[0];
worker_count++;
if (verbose_mode > 0)
fprintf(log_fh, "worker[%d] [slot=%d] started\n", pid, j);;
}
} else
#endif
{
/* Single process mode, process one file at a time... */
res = optimize(log_fh, filename, newname, tmpdir, &file_stat, &rate, &saved);
if (res == 0) {
average_count++;
average_rate += rate;
total_save += saved;
} else if (res == 1) {
decompress_err_count++;
} else if (res == 2) {
compress_err_count++;
}
}
} while (files_from || ++i < argc);
#ifdef PARALLEL_PROCESSING
/* Wait for any child processes to exit... */
if (max_workers > 1) {
if (verbose_mode) {
fprintf(log_fh, "Waiting for %d workers to finish...\n", worker_count);
}
while ((pid = wait_for_worker(log_fh)) > 0) {
if (verbose_mode > 2)
fprintf(log_fh, "worker[%d] done\n", pid);
}
}
#endif
if (totals_mode && !quiet_mode)
fprintf(log_fh, "Average ""compression"" (%ld files): %0.2f%% (total saved %0.0fk)\n",
average_count, average_rate/average_count, total_save);
return (decompress_err_count > 0 || compress_err_count > 0 ? 1 : 0);;
}
/* eof :-) */