diff --git a/README.md b/README.md index cfe0b49..ebb3cd5 100644 --- a/README.md +++ b/README.md @@ -78,6 +78,11 @@ SmallFry | `-m smallfry` | Linear-weighted BBCQ-like ([original project](https:/ **Note**: The SmallFry algorithm may be [patented](http://www.jpegmini.com/main/technology) so use with caution. +#### Subsampling +The JPEG format allows for subsampling of the color channels to save space. For each 2x2 block of pixels per color channel (four pixels total) it can store four pixels (all of them), two pixels or a single pixel. By default, the JPEG encoder subsamples the non-luma channels to two pixels (often referred to as 4:2:0 subsampling). Most digital cameras do the same because of limitations in the human eye. This may lead to unintended behavior for specific use cases (see #12 for an example), so you can use `--subsample disable` to disable this subsampling. + +#### Example Commands + ```bash # Default settings jpeg-recompress image.jpg compressed.jpg @@ -91,6 +96,9 @@ jpeg-recompress --accurate --quality high --min 60 image.jpg compressed.jpg # Use SmallFry instead of SSIM jpeg-recompress --method smallfry image.jpg compressed.jpg +# Use 4:4:4 sampling (disables subsampling). +jpeg-recmopress --subsample disable image.jpg compressed.jpg + # Remove fisheye distortion (Tokina 10-17mm on APS-C @ 10mm) jpeg-recompress --defish 2.6 --zoom 1.2 image.jpg defished.jpg diff --git a/jpeg-recompress.c b/jpeg-recompress.c index 3619c76..4606a2e 100644 --- a/jpeg-recompress.c +++ b/jpeg-recompress.c @@ -65,6 +65,9 @@ int copyFiles = 1; // Whether to favor accuracy over speed int accurate = 0; +// Chroma subsampling method +int subsample = SUBSAMPLE_DEFAULT; + static void setAttempts(command_t *self) { attempts = atoi(self->arg); } @@ -206,6 +209,16 @@ static void setTargetFromPreset() { } } +static void setSubsampling(command_t *self) { + if (!strcmp("default", self->arg)) { + subsample = SUBSAMPLE_DEFAULT; + } else if (!strcmp("disable", self->arg)) { + subsample = SUBSAMPLE_444; + } else { + fprintf(stderr, "Unknown sampling method '%s', using default!\n", self->arg); + } +} + // Open a file for writing FILE *openOutput(char *name) { if (strcmp("-", name) == 0) { @@ -249,6 +262,7 @@ int main (int argc, char **argv) { command_option(&cmd, "-r", "--ppm", "Parse input as PPM instead of JPEG", setPpm); command_option(&cmd, "-c", "--no-copy", "Disable copying files that will not be compressed", setCopyFiles); command_option(&cmd, "-p", "--no-progressive", "Disable progressive encoding", setNoProgressive); + command_option(&cmd, "-S", "--subsample [arg]", "Set subsampling method. Valid values: 'default', 'disable'. [default]", setSubsampling); command_parse(&cmd, argc, argv); if (cmd.argc < 2) { @@ -329,7 +343,7 @@ int main (int argc, char **argv) { int optimize = accurate ? 1 : (attempt ? 0 : 1); // Recompress to a new quality level, without optimizations (for speed) - compressedSize = encodeJpeg(&compressed, original, width, height, JCS_RGB, quality, progressive, optimize); + compressedSize = encodeJpeg(&compressed, original, width, height, JCS_RGB, quality, progressive, optimize, subsample); // Load compressed luma for quality comparison compressedGraySize = decodeJpeg(compressed, compressedSize, &compressedGray, &width, &height, JCS_GRAYSCALE); diff --git a/src/util.c b/src/util.c index 29b5f7f..946ae95 100644 --- a/src/util.c +++ b/src/util.c @@ -104,7 +104,7 @@ unsigned long decodeJpeg(unsigned char *buf, unsigned long bufSize, unsigned cha return row_stride * (*height); } -unsigned long encodeJpeg(unsigned char **jpeg, unsigned char *buf, int width, int height, int pixelFormat, int quality, int progressive, int optimize) { +unsigned long encodeJpeg(unsigned char **jpeg, unsigned char *buf, int width, int height, int pixelFormat, int quality, int progressive, int optimize, int subsample) { long unsigned int jpegSize = 0; struct jpeg_compress_struct cinfo; struct jpeg_error_mgr jerr; @@ -162,6 +162,15 @@ unsigned long encodeJpeg(unsigned char **jpeg, unsigned char *buf, int width, in jpeg_simple_progression(&cinfo); } + if (subsample == SUBSAMPLE_444) { + cinfo.comp_info[0].h_samp_factor = 1; + cinfo.comp_info[0].v_samp_factor = 1; + cinfo.comp_info[1].h_samp_factor = 1; + cinfo.comp_info[1].v_samp_factor = 1; + cinfo.comp_info[2].h_samp_factor = 1; + cinfo.comp_info[2].v_samp_factor = 1; + } + jpeg_set_quality(&cinfo, quality, TRUE); // Start the compression diff --git a/src/util.h b/src/util.h index d8cb35b..2c91110 100644 --- a/src/util.h +++ b/src/util.h @@ -10,6 +10,20 @@ const char *VERSION; +// Subsampling method, which defines how much of the data from +// each color channel is included in the image per 2x2 block. +// A value of 4 means all four pixels are included, while 2 +// means that only two of the four are included (hence the term +// subsampling). Subsampling works really well for photos, but +// can have issues with crisp colored borders (e.g. red text). +enum SUBSAMPLING_METHOD { + // Default is 4:2:0 + SUBSAMPLE_DEFAULT, + // Using 4:4:4 is more detailed and will prevent fine text + // from getting blurry (e.g. screenshots) + SUBSAMPLE_444 +}; + /* Read a file into a buffer and return the length. */ @@ -33,7 +47,7 @@ unsigned long decodePpm(unsigned char *buf, unsigned long bufSize, unsigned char /* Encode a buffer of image pixels into a JPEG. */ -unsigned long encodeJpeg(unsigned char **jpeg, unsigned char *buf, int width, int height, int pixelFormat, int quality, int progressive, int optimize); +unsigned long encodeJpeg(unsigned char **jpeg, unsigned char *buf, int width, int height, int pixelFormat, int quality, int progressive, int optimize, int subsample); /* Get JPEG metadata (EXIF, IPTC, XMP, etc) and return a buffer