blob: 6bea7e1e20a2d09e66cf9d65d552b91d28cd9132 [file] [log] [blame]
/*
* Copyright 2009 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// Author: Bryan McQuade, Matthew Steele
#include "pagespeed/kernel/image/jpeg_optimizer.h"
#include <setjmp.h>
// 'stdio.h' provides FILE for jpeglib (needed for certain builds)
#include <stdio.h>
#include <algorithm>
#include <cstdlib>
#include "base/logging.h"
#include "pagespeed/kernel/base/message_handler.h"
#include "pagespeed/kernel/base/string.h"
#include "pagespeed/kernel/image/jpeg_reader.h"
extern "C" {
#ifdef USE_SYSTEM_LIBJPEG
#include "jpeglib.h" // NOLINT
#else
#include "third_party/libjpeg_turbo/src/jpeglib.h"
#endif
}
using net_instaweb::MessageHandler;
using pagespeed::image_compression::ColorSampling;
using pagespeed::image_compression::JpegCompressionOptions;
using pagespeed::image_compression::JpegLossyOptions;
using pagespeed::image_compression::RETAIN;
using pagespeed::image_compression::YUV420;
using pagespeed::image_compression::YUV422;
using pagespeed::image_compression::YUV444;
namespace {
// Unfortunately, libjpeg normally only supports writing images to C FILE
// pointers, wheras we want to write to a C++ string. Fortunately, libjpeg
// also provides an extension mechanism. Below, we define a new kind of
// jpeg_destination_mgr for writing to strings.
// The below code was adapted from the JPEGMemoryReader class that can be found
// in src/o3d/core/cross/bitmap_jpg.cc in the Chromium source tree (r29423).
// That code is Copyright 2009, Google Inc.
#define DESTINATION_MANAGER_BUFFER_SIZE 4096
struct DestinationManager : public jpeg_destination_mgr {
JOCTET buffer[DESTINATION_MANAGER_BUFFER_SIZE];
GoogleString *str;
};
METHODDEF(void) InitDestination(j_compress_ptr cinfo) {
DestinationManager &dest =
*reinterpret_cast<DestinationManager*>(cinfo->dest);
dest.next_output_byte = dest.buffer;
dest.free_in_buffer = DESTINATION_MANAGER_BUFFER_SIZE;
};
METHODDEF(boolean) EmptyOutputBuffer(j_compress_ptr cinfo) {
DestinationManager &dest =
*reinterpret_cast<DestinationManager*>(cinfo->dest);
dest.str->append(reinterpret_cast<char*>(dest.buffer),
DESTINATION_MANAGER_BUFFER_SIZE);
dest.free_in_buffer = DESTINATION_MANAGER_BUFFER_SIZE;
dest.next_output_byte = dest.buffer;
return TRUE;
};
METHODDEF(void) TermDestination(j_compress_ptr cinfo) {
DestinationManager &dest =
*reinterpret_cast<DestinationManager*>(cinfo->dest);
const size_t datacount =
DESTINATION_MANAGER_BUFFER_SIZE - dest.free_in_buffer;
if (datacount > 0) {
dest.str->append(reinterpret_cast<char*>(dest.buffer), datacount);
}
};
// Call this function on a j_compress_ptr to install a writer that will write
// to the given string.
void JpegStringWriter(j_compress_ptr cinfo, GoogleString *data_dest) {
if (cinfo->dest == NULL) {
cinfo->dest = (struct jpeg_destination_mgr*)
(*cinfo->mem->alloc_small) ((j_common_ptr) cinfo, JPOOL_PERMANENT,
sizeof(DestinationManager));
}
DestinationManager &dest =
*reinterpret_cast<DestinationManager*>(cinfo->dest);
dest.str = data_dest;
dest.init_destination = InitDestination;
dest.empty_output_buffer = EmptyOutputBuffer;
dest.term_destination = TermDestination;
}
// ErrorExit() is installed as a callback, called on errors
// encountered within libjpeg. The longjmp jumps back
// to the setjmp in JpegOptimizer::CreateOptimizedJpeg().
void ErrorExit(j_common_ptr jpeg_state_struct) {
jmp_buf *env = static_cast<jmp_buf *>(jpeg_state_struct->client_data);
(*jpeg_state_struct->err->output_message)(jpeg_state_struct);
if (env)
longjmp(*env, 1);
}
// OutputMessageFromReader is called by libjpeg code on an error when reading.
// Without this function, a default function would print to standard error.
void OutputMessage(j_common_ptr jpeg_decompress) {
// The following code is handy for debugging.
/*
char buf[JMSG_LENGTH_MAX];
(*jpeg_decompress->err->format_message)(jpeg_decompress, buf);
DLOG(INFO) << "JPEG Reader Error: " << buf;
*/
}
// Marker for APPN segment is obtained by adding N to JPEG_APP0.
const int kColorProfileMarker = JPEG_APP0 + 2;
const int kExifDataMarker = JPEG_APP0 + 1;
// Signifies max bytes that needs to read, while reading jpeg segments like exif
// data, color profiles and etc.
const int kMaxSegmentSize = 0xFFFF;
// Initializes the jpeg compress struct.
void InitJpegCompress(j_compress_ptr cinfo, jpeg_error_mgr* compress_error) {
memset(cinfo, 0, sizeof(jpeg_compress_struct));
memset(compress_error, 0, sizeof(jpeg_error_mgr));
cinfo->err = jpeg_std_error(compress_error);
compress_error->error_exit = &ErrorExit;
compress_error->output_message = &OutputMessage;
jpeg_create_compress(cinfo);
}
void SetJpegCompressBeforeStartCompress(const JpegCompressionOptions& options,
const jpeg_decompress_struct* jpeg_decompress,
jpeg_compress_struct* jpeg_compress) {
if (options.lossy) {
const JpegLossyOptions& lossy_options = options.lossy_options;
// Set the compression parameters if set and lossy compression is enabled,
// else use the defaults. Last parameter to jpeg_set_quality resticts the
// jpeg quantizer values in 8 bit values, even though jpeg support 12 bit
// quantizer values, it is not supported widely.
jpeg_set_quality(jpeg_compress, lossy_options.quality, 1);
// Set the color subsampling if applicable.
if (jpeg_compress->jpeg_color_space == JCS_YCbCr) {
// Set the color sampling.
if (lossy_options.color_sampling == YUV444) {
jpeg_compress->comp_info[0].h_samp_factor = 1;
jpeg_compress->comp_info[0].v_samp_factor = 1;
} else if (lossy_options.color_sampling == YUV422) {
jpeg_compress->comp_info[0].h_samp_factor = 2;
jpeg_compress->comp_info[0].v_samp_factor = 1;
} else if (lossy_options.color_sampling == YUV420) {
jpeg_compress->comp_info[0].h_samp_factor = 2;
jpeg_compress->comp_info[0].v_samp_factor = 2;
} else if (lossy_options.color_sampling == RETAIN &&
jpeg_decompress != NULL) {
// Retain the input.
for (int idx = 0; idx < jpeg_compress->num_components; ++idx) {
jpeg_compress->comp_info[idx].h_samp_factor =
jpeg_decompress->comp_info[idx].h_samp_factor;
jpeg_compress->comp_info[idx].v_samp_factor =
jpeg_decompress->comp_info[idx].v_samp_factor;
}
}
}
}
if (options.progressive) {
jpeg_simple_progression(jpeg_compress);
if (options.lossy && options.lossy_options.num_scans > 0) {
// We can honour the num scans only if the number of scans we want is less
// than or equals to total number of scans defined for this image, else
// compress will fail.
jpeg_compress->num_scans = std::min(jpeg_compress->num_scans,
options.lossy_options.num_scans);
}
}
}
void SetJpegCompressAfterStartCompress(const JpegCompressionOptions& options,
const jpeg_decompress_struct& jpeg_decompress,
jpeg_compress_struct* jpeg_compress) {
if (options.retain_color_profile || options.retain_exif_data) {
jpeg_saved_marker_ptr marker;
for (marker = jpeg_decompress.marker_list; marker != NULL;
marker = marker->next) {
// We only copy these headers if present in the decompress struct.
if ((marker->marker == kExifDataMarker && options.retain_exif_data) ||
(marker->marker == kColorProfileMarker &&
options.retain_color_profile)) {
jpeg_write_marker(jpeg_compress, marker->marker, marker->data,
marker->data_length);
}
}
}
}
class JpegOptimizer {
public:
explicit JpegOptimizer(MessageHandler* handler);
~JpegOptimizer();
// Take the given input file and compress it, either losslessly or lossily,
// depending on the passed in options. Note that the options parameter can be
// null, in which case the default options are used.
// If this function fails (returns false), it can be called again.
// @return true on success, false on failure.
bool CreateOptimizedJpeg(const GoogleString &original,
GoogleString *compressed,
const JpegCompressionOptions& options);
private:
bool DoCreateOptimizedJpeg(const GoogleString &original,
jpeg_decompress_struct *jpeg_decompress,
GoogleString *compressed,
const JpegCompressionOptions& options);
bool OptimizeLossless(jpeg_decompress_struct *jpeg_decompress,
GoogleString *compressed,
const JpegCompressionOptions& options);
bool OptimizeLossy(jpeg_decompress_struct *jpeg_decompress,
GoogleString *compressed,
const JpegCompressionOptions& options);
// Structures for jpeg compression.
jpeg_compress_struct jpeg_compress_;
jpeg_error_mgr compress_error_;
MessageHandler* message_handler_;
pagespeed::image_compression::JpegReader reader_;
DISALLOW_COPY_AND_ASSIGN(JpegOptimizer);
};
JpegOptimizer::JpegOptimizer(MessageHandler* handler)
: message_handler_(handler),
reader_(handler) {
InitJpegCompress(&jpeg_compress_, &compress_error_);
}
JpegOptimizer::~JpegOptimizer() {
jpeg_destroy_compress(&jpeg_compress_);
}
bool JpegOptimizer::OptimizeLossy(
jpeg_decompress_struct *jpeg_decompress,
GoogleString *compressed,
const JpegCompressionOptions& options) {
if (!options.lossy) {
PS_LOG_DFATAL(message_handler_, \
"lossy is not set in options for lossy jpeg compression");
return false;
}
// Copy data from the source to the dest.
jpeg_compress_.image_width = jpeg_decompress->image_width;
jpeg_compress_.image_height = jpeg_decompress->image_height;
jpeg_compress_.input_components = jpeg_decompress->num_components;
// Persist the input file's colorspace.
jpeg_decompress->out_color_space = jpeg_decompress->jpeg_color_space;
jpeg_compress_.in_color_space = jpeg_decompress->jpeg_color_space;
// Set the default options.
jpeg_set_defaults(&jpeg_compress_);
// Set optimize huffman to true.
jpeg_compress_.optimize_coding = TRUE;
SetJpegCompressBeforeStartCompress(options, jpeg_decompress, &jpeg_compress_);
// Prepare to write to a string.
JpegStringWriter(&jpeg_compress_, compressed);
jpeg_start_compress(&jpeg_compress_, TRUE);
jpeg_start_decompress(jpeg_decompress);
// Write any markers if needed.
SetJpegCompressAfterStartCompress(options, *jpeg_decompress, &jpeg_compress_);
// Make sure input/output parameters are configured correctly.
DCHECK(jpeg_compress_.image_width == jpeg_decompress->output_width);
DCHECK(jpeg_compress_.image_height == jpeg_decompress->output_height);
DCHECK(jpeg_compress_.input_components == jpeg_decompress->output_components);
DCHECK(jpeg_compress_.in_color_space == jpeg_decompress->out_color_space);
bool valid_jpeg = true;
JSAMPROW row_pointer[1];
row_pointer[0] = static_cast<JSAMPLE*>(malloc(
jpeg_decompress->output_width * jpeg_decompress->output_components));
while (jpeg_compress_.next_scanline < jpeg_compress_.image_height) {
const JDIMENSION num_scanlines_read =
jpeg_read_scanlines(jpeg_decompress, row_pointer, 1);
if (num_scanlines_read != 1) {
valid_jpeg = false;
break;
}
if (jpeg_write_scanlines(&jpeg_compress_, row_pointer, 1) != 1) {
// We failed to write all the row. Abort.
valid_jpeg = false;
break;
}
}
free(row_pointer[0]);
return valid_jpeg;
}
bool JpegOptimizer::OptimizeLossless(jpeg_decompress_struct *jpeg_decompress,
GoogleString *compressed, const JpegCompressionOptions& options) {
if (options.lossy) {
PS_LOG_DFATAL(message_handler_, \
"Lossy options are not allowed in lossless compression.");
return false;
}
jvirt_barray_ptr *coefficients = jpeg_read_coefficients(jpeg_decompress);
bool valid_jpeg = (coefficients != NULL);
if (valid_jpeg) {
// Copy data from the source to the dest.
jpeg_copy_critical_parameters(jpeg_decompress, &jpeg_compress_);
SetJpegCompressBeforeStartCompress(options, jpeg_decompress,
&jpeg_compress_);
// Set optimize huffman to true.
jpeg_compress_.optimize_coding = TRUE;
// Prepare to write to a string.
JpegStringWriter(&jpeg_compress_, compressed);
// Copy the coefficients into the compression struct.
jpeg_write_coefficients(&jpeg_compress_, coefficients);
// Write any markers if needed.
SetJpegCompressAfterStartCompress(options, *jpeg_decompress,
&jpeg_compress_);
}
return valid_jpeg;
}
// Helper for JpegOptimizer::CreateOptimizedJpeg(). This function does the
// work, and CreateOptimizedJpeg() does some cleanup.
bool JpegOptimizer::DoCreateOptimizedJpeg(
const GoogleString &original,
jpeg_decompress_struct *jpeg_decompress,
GoogleString *compressed,
const pagespeed::image_compression::JpegCompressionOptions& options) {
// libjpeg's error handling mechanism requires that longjmp be used
// to get control after an error.
jmp_buf env;
if (setjmp(env)) {
// This code is run only when libjpeg hit an error, and called
// longjmp(env). Returning false will cause jpeg_abort_(de)compress to be
// called on jpeg_(de)compress_, putting those structures back into a state
// where they can be used again.
return false;
}
// Need to install env so that it will be longjmp()ed to on error.
jpeg_decompress->client_data = static_cast<void *>(&env);
jpeg_compress_.client_data = static_cast<void *>(&env);
reader_.PrepareForRead(original.data(), original.size());
if (options.retain_color_profile) {
jpeg_save_markers(jpeg_decompress, kColorProfileMarker, kMaxSegmentSize);
}
if (options.retain_exif_data) {
jpeg_save_markers(jpeg_decompress, kExifDataMarker, kMaxSegmentSize);
}
// Read jpeg data into the decompression struct.
jpeg_read_header(jpeg_decompress, TRUE);
bool valid_jpeg = false;
if (options.lossy) {
valid_jpeg = OptimizeLossy(jpeg_decompress, compressed, options);
} else {
valid_jpeg = OptimizeLossless(jpeg_decompress, compressed, options);
}
// Finish the compression process.
jpeg_finish_compress(&jpeg_compress_);
jpeg_finish_decompress(jpeg_decompress);
return valid_jpeg;
}
bool JpegOptimizer::CreateOptimizedJpeg(const GoogleString &original,
GoogleString *compressed, const JpegCompressionOptions& options) {
jpeg_decompress_struct* jpeg_decompress = reader_.decompress_struct();
bool result = DoCreateOptimizedJpeg(original, jpeg_decompress, compressed,
options);
jpeg_decompress->client_data = NULL;
jpeg_compress_.client_data = NULL;
if (!result) {
// Clean up the state of jpeglib structures. It is okay to abort even if
// no (de)compression is in progress. This is crucial because we enter
// this block even if no jpeg-related error happened.
jpeg_abort_decompress(jpeg_decompress);
jpeg_abort_compress(&jpeg_compress_);
}
return result;
}
} // namespace
namespace pagespeed {
namespace image_compression {
struct JpegScanlineWriter::Data {
Data() {
InitJpegCompress(&jpeg_compress_, &compress_error_);
}
~Data() {
jpeg_destroy_compress(&jpeg_compress_);
}
// Structures for jpeg compression.
jpeg_compress_struct jpeg_compress_;
jpeg_error_mgr compress_error_;
};
JpegScanlineWriter::JpegScanlineWriter(MessageHandler* handler)
: data_(new Data()),
message_handler_(handler) {
}
JpegScanlineWriter::~JpegScanlineWriter() {
delete data_;
}
void JpegScanlineWriter::SetJmpBufEnv(jmp_buf* env) {
data_->jpeg_compress_.client_data = static_cast<void *>(env);
}
ScanlineStatus JpegScanlineWriter::InitWithStatus(const size_t width,
const size_t height,
PixelFormat pixel_format) {
data_->jpeg_compress_.image_width = width;
data_->jpeg_compress_.image_height = height;
switch (pixel_format) {
case RGB_888:
data_->jpeg_compress_.input_components = 3;
data_->jpeg_compress_.in_color_space = JCS_RGB;
break;
case GRAY_8:
data_->jpeg_compress_.input_components = 1;
data_->jpeg_compress_.in_color_space = JCS_GRAYSCALE;
break;
case RGBA_8888:
return PS_LOGGED_STATUS(PS_DLOG_INFO, message_handler_,
SCANLINE_STATUS_UNSUPPORTED_FEATURE,
SCANLINE_JPEGWRITER, "transparency");
break;
default:
return PS_LOGGED_STATUS(PS_LOG_DFATAL, message_handler_,
SCANLINE_STATUS_INTERNAL_ERROR,
SCANLINE_JPEGWRITER,
"unknown pixel format: %s",
GetPixelFormatString(pixel_format));
}
// Set the default options.
jpeg_set_defaults(&data_->jpeg_compress_);
// Set optimize huffman to true.
data_->jpeg_compress_.optimize_coding = TRUE;
return ScanlineStatus(SCANLINE_STATUS_SUCCESS);
}
void JpegScanlineWriter::SetJpegCompressParams(
const JpegCompressionOptions& options) {
if (!options.lossy) {
PS_LOG_DFATAL(message_handler_, \
"Unable to perform lossless encoding in JpegScanlineWriter." \
" Using jpeg default lossy encoding options.");
}
SetJpegCompressBeforeStartCompress(options, NULL, &data_->jpeg_compress_);
}
ScanlineStatus JpegScanlineWriter::InitializeWriteWithStatus(
const void* const params,
GoogleString * const compressed) {
if (params == NULL) {
return PS_LOGGED_STATUS(PS_LOG_DFATAL, message_handler_,
SCANLINE_STATUS_INVOCATION_ERROR,
SCANLINE_JPEGWRITER,
"missing JpegCompressionOptions*");
}
const JpegCompressionOptions* jpeg_compression_options =
static_cast<const JpegCompressionOptions*>(params);
SetJpegCompressParams(*jpeg_compression_options);
JpegStringWriter(&data_->jpeg_compress_, compressed);
jpeg_start_compress(&data_->jpeg_compress_, TRUE);
return ScanlineStatus(SCANLINE_STATUS_SUCCESS);
}
ScanlineStatus JpegScanlineWriter::WriteNextScanlineWithStatus(
const void* const scanline_bytes) {
JSAMPROW row_pointer[1] = {
static_cast<JSAMPLE*>(
const_cast<void*>(scanline_bytes))
};
unsigned int result = jpeg_write_scanlines(&data_->jpeg_compress_,
row_pointer, 1);
if (result == 1) {
return ScanlineStatus(SCANLINE_STATUS_SUCCESS);
} else {
return PS_LOGGED_STATUS(PS_LOG_ERROR, message_handler_,
SCANLINE_STATUS_INTERNAL_ERROR,
SCANLINE_JPEGWRITER,
"jpeg_write_scanlines()");
}
}
ScanlineStatus JpegScanlineWriter::FinalizeWriteWithStatus() {
jpeg_finish_compress(&data_->jpeg_compress_);
return ScanlineStatus(SCANLINE_STATUS_SUCCESS);
}
void JpegScanlineWriter::AbortWrite() {
data_->jpeg_compress_.client_data = NULL;
jpeg_abort_compress(&data_->jpeg_compress_);
}
bool OptimizeJpeg(const GoogleString &original,
GoogleString *compressed,
MessageHandler* handler) {
JpegOptimizer optimizer(handler);
JpegCompressionOptions options;
return optimizer.CreateOptimizedJpeg(original, compressed, options);
}
bool OptimizeJpegWithOptions(const GoogleString &original,
GoogleString *compressed,
const JpegCompressionOptions &options,
MessageHandler* handler) {
JpegOptimizer optimizer(handler);
return optimizer.CreateOptimizedJpeg(original, compressed, options);
}
} // namespace image_compression
} // namespace pagespeed