blob: 2496a76028167585d9910b86f372e8819cb4f975 [file] [log] [blame]
/**
* Copyright 2010 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: jmaessen@google.com (Jan Maessen)
#include "net/instaweb/rewriter/public/image.h"
#include "base/basictypes.h"
#include "net/instaweb/rewriter/public/resource.h"
#include "net/instaweb/rewriter/public/resource_manager.h"
#include "net/instaweb/util/public/content_type.h"
#include "net/instaweb/util/public/file_system.h"
#include "net/instaweb/util/public/message_handler.h"
#include <string>
#include "net/instaweb/util/public/string_util.h"
#include "net/instaweb/util/public/writer.h"
#undef PAGESPEED_PNG_OPTIMIZER_GIF_READER
#define PAGESPEED_PNG_OPTIMIZER_GIF_READER 0
#ifdef USE_SYSTEM_OPENCV
#include "cv.h"
#include "highgui.h"
#else
#include "third_party/opencv/src/opencv/include/opencv/cv.h"
#include "third_party/opencv/src/opencv/include/opencv/highgui.h"
#endif
#include "pagespeed/image_compression/gif_reader.h"
#include "pagespeed/image_compression/jpeg_optimizer.h"
#include "pagespeed/image_compression/png_optimizer.h"
namespace net_instaweb {
namespace ImageHeaders {
const char kPngHeader[] = "\x89PNG\r\n\x1a\n";
const size_t kPngHeaderLength = sizeof(kPngHeader) - 1;
const char kPngIHDR[] = "\0\0\0\x0dIHDR";
const size_t kPngIHDRLength = sizeof(kPngIHDR) - 1;
const size_t kIHDRDataStart = kPngHeaderLength + kPngIHDRLength;
const size_t kPngIntSize = 4;
const char kGifHeader[] = "GIF8";
const size_t kGifHeaderLength = sizeof(kGifHeader) - 1;
const size_t kGifDimStart = kGifHeaderLength + 2;
const size_t kGifIntSize = 2;
const size_t kJpegIntSize = 2;
} // namespace ImageHeaders
namespace {
bool WriteTempFileWithContentType(
const StringPiece& prefix_name, const ContentType& content_type,
const StringPiece& buffer, std::string* filename,
FileSystem* file_system, MessageHandler* handler) {
std::string tmp_filename;
bool ok = file_system->WriteTempFile(
prefix_name.as_string().c_str(), buffer, &tmp_filename, handler);
if (ok) {
*filename = StrCat(tmp_filename, content_type.file_extension());
ok = file_system->RenameFile(
tmp_filename.c_str(), filename->c_str(), handler);
}
return ok;
}
} // namespace
Image::Image(const StringPiece& original_contents,
const std::string& url,
const StringPiece& file_prefix,
FileSystem* file_system,
MessageHandler* handler)
: file_prefix_(file_prefix.data(), file_prefix.size()),
file_system_(file_system),
handler_(handler),
image_type_(IMAGE_UNKNOWN),
original_contents_(original_contents),
output_contents_(),
output_valid_(false),
opencv_filename_(),
opencv_image_(NULL),
opencv_load_possible_(true),
resized_(false),
url_(url) { }
Image::~Image() {
CleanOpenCV();
}
// Looks through blocks of jpeg stream to find SOFn block
// indicating encoding and dimensions of image.
// Loosely based on code and FAQs found here:
// http://www.faqs.org/faqs/jpeg-faq/part1/
void Image::FindJpegSize() {
const StringPiece& buf = original_contents_;
size_t pos = 2; // Position of first data block after header.
while (pos < buf.size()) {
// Read block identifier
int id = CharToInt(buf[pos++]);
if (id == 0xff) { // Padding byte
continue;
}
// At this point pos points to first data byte in block. In any block,
// first two data bytes are size (including these 2 bytes). But first,
// make sure block wasn't truncated on download.
if (pos + ImageHeaders::kJpegIntSize > buf.size()) {
break;
}
int length = JpegIntAtPosition(buf, pos);
// Now check for a SOFn header, which describes image dimensions.
if (0xc0 <= id && id <= 0xcf && // SOFn header
length >= 8 && // Valid SOFn block size
pos + 1 + 3 * ImageHeaders::kJpegIntSize <= buf.size() &&
// Above avoids case where dimension data was truncated
id != 0xc4 && id != 0xc8 && id != 0xcc) {
// 0xc4, 0xc8, 0xcc aren't actually valid SOFn headers.
// NOTE: we don't care if we have the whole SOFn block,
// just that we can fetch both dimensions without trouble.
// Our image download could be truncated at this point for
// all we care.
// We're a bit sloppy about SOFn block size, as it's
// actually 8 + 3 * buf[pos+2], but for our purposes this
// will suffice as we don't parse subsequent metadata (which
// describes the formatting of chunks of image data).
int height = JpegIntAtPosition(buf, pos + 1 + ImageHeaders::kJpegIntSize);
int width = JpegIntAtPosition(buf,
pos + 1 + 2 * ImageHeaders::kJpegIntSize);
dims_.set_dims(width, height);
break;
}
pos += length;
}
if (dims_.height() <= 0 || dims_.width() <= 0) {
dims_.invalidate();
handler_->Error(url_.c_str(), 0,
"Couldn't find jpeg dimensions (data truncated?).");
}
}
// Looks at first (IHDR) block of png stream to find image dimensions.
// See also: http://www.w3.org/TR/PNG/
void Image::FindPngSize() {
const StringPiece& buf = original_contents_;
if ((buf.size() >= // Not truncated
ImageHeaders::kIHDRDataStart + 2 * ImageHeaders::kPngIntSize) &&
(StringPiece(buf.data() + ImageHeaders::kPngHeaderLength,
ImageHeaders::kPngIHDRLength) ==
StringPiece(ImageHeaders::kPngIHDR, ImageHeaders::kPngIHDRLength))) {
int width = PngIntAtPosition(buf, ImageHeaders::kIHDRDataStart);
int height = PngIntAtPosition(
buf, ImageHeaders::kIHDRDataStart + ImageHeaders::kPngIntSize);
dims_.set_dims(width, height);
} else {
handler_->Error(url_.c_str(), 0,
"Couldn't find png dimensions "
"(data truncated or IHDR missing).");
}
}
// Looks at header of GIF file to extract image dimensions.
// See also: http://en.wikipedia.org/wiki/Graphics_Interchange_Format
void Image::FindGifSize() {
const StringPiece& buf = original_contents_;
if (buf.size() >=
ImageHeaders::kGifDimStart + 2 * ImageHeaders::kGifIntSize) {
// Not truncated
int width = GifIntAtPosition(buf, ImageHeaders::kGifDimStart);
int height = GifIntAtPosition(
buf, ImageHeaders::kGifDimStart + ImageHeaders::kGifIntSize);
dims_.set_dims(width, height);
} else {
handler_->Error(url_.c_str(), 0,
"Couldn't find gif dimensions (data truncated)");
}
}
// Looks at image data in order to determine image type, and also fills in any
// dimension information it can (setting image_type_ and dims_).
void Image::ComputeImageType() {
// Image classification based on buffer contents gakked from leptonica,
// but based on well-documented headers (see Wikipedia etc.).
// Note that we can be fooled if we're passed random binary data;
// we make the call based on as few as two bytes (JPEG).
const StringPiece& buf = original_contents_;
if (buf.size() >= 8) {
// Note that gcc rightly complains about constant ranges with the
// negative char constants unless we cast.
switch (CharToInt(buf[0])) {
case 0xff:
// Either jpeg or jpeg2
// (the latter we don't handle yet, and don't bother looking for).
if (CharToInt(buf[1]) == 0xd8) {
image_type_ = IMAGE_JPEG;
FindJpegSize();
}
break;
case 0x89:
// Possible png.
if (StringPiece(buf.data(), ImageHeaders::kPngHeaderLength) ==
StringPiece(ImageHeaders::kPngHeader,
ImageHeaders::kPngHeaderLength)) {
image_type_ = IMAGE_PNG;
FindPngSize();
}
break;
case 'G':
// Possible gif.
if ((StringPiece(buf.data(), ImageHeaders::kGifHeaderLength) ==
StringPiece(ImageHeaders::kGifHeader,
ImageHeaders::kGifHeaderLength)) &&
(buf[4] == '7' || buf[4] == '9') && buf[5] == 'a') {
image_type_ = IMAGE_GIF;
FindGifSize();
}
break;
default:
break;
}
}
}
const ContentType* Image::content_type() {
const ContentType* res = NULL;
switch (image_type()) {
case IMAGE_UNKNOWN:
break;
case IMAGE_JPEG:
res = &kContentTypeJpeg;
break;
case IMAGE_PNG:
res = &kContentTypePng;
break;
case IMAGE_GIF:
res = &kContentTypeGif;
break;
}
return res;
}
// Makes sure OpenCV version of image is loaded if that is possible.
// Returns value of opencv_load_possible_ after load attempted.
// Note that if the load fails, opencv_load_possible_ will be false
// and future calls to LoadOpenCV will fail fast.
bool Image::LoadOpenCV() {
if (opencv_image_ == NULL && opencv_load_possible_) {
Image::Type image_type = this->image_type();
const ContentType* content_type = this->content_type();
opencv_load_possible_ = (content_type != NULL &&
image_type != IMAGE_GIF);
if (opencv_load_possible_) {
opencv_load_possible_ =
WriteTempFileWithContentType(
file_prefix_, *content_type,
original_contents_, &opencv_filename_,
file_system_, handler_);
}
if (opencv_load_possible_) {
opencv_image_ = cvLoadImage(opencv_filename_.c_str());
file_system_->RemoveFile(opencv_filename_.c_str(), handler_);
opencv_load_possible_ = (opencv_image_ != NULL);
}
}
if (opencv_load_possible_) {
// A bit of belt and suspenders dimension checking. We used to do this for
// every image we loaded, but now we only do it when we're already paying
// the cost of OpenCV image conversion.
if (dims_.valid() && dims_.width() != opencv_image_->width) {
handler_->Error(url_.c_str(), 0,
"Computed width %d doesn't match OpenCV %d",
dims_.width(), opencv_image_->width);
}
if (dims_.valid() && dims_.height() != opencv_image_->height) {
handler_->Error(url_.c_str(), 0,
"Computed height %d doesn't match OpenCV %d",
dims_.height(), opencv_image_->height);
}
}
return opencv_load_possible_;
}
// Get rid of OpenCV image data gracefully (requires a call to OpenCV).
void Image::CleanOpenCV() {
if (opencv_image_ != NULL) {
cvReleaseImage(&opencv_image_);
}
}
void Image::Dimensions(ImageDim* natural_dim) {
if (dims_.valid()) {
natural_dim->set_dims(dims_.width(), dims_.height());
} else {
natural_dim->invalidate();
}
}
bool Image::ResizeTo(const ImageDim& new_dim) {
CHECK(new_dim.valid());
if (resized_) {
// If we already resized, drop data and work with original image.
UndoResize();
}
bool ok = opencv_image_ != NULL || LoadOpenCV();
if (ok) {
IplImage* rescaled_image =
cvCreateImage(cvSize(new_dim.width(), new_dim.height()),
opencv_image_->depth,
opencv_image_->nChannels);
ok = rescaled_image != NULL;
if (ok) {
cvResize(opencv_image_, rescaled_image);
cvReleaseImage(&opencv_image_);
opencv_image_ = rescaled_image;
}
resized_ = ok;
}
return resized_;
}
void Image::UndoResize() {
if (resized_) {
CleanOpenCV();
output_valid_ = false;
image_type_ = IMAGE_UNKNOWN;
resized_ = false;
}
}
// Performs image optimization and output
bool Image::ComputeOutputContents() {
if (!output_valid_) {
bool ok = true;
std::string opencv_contents;
StringPiece contents = original_contents_;
// Choose appropriate source for image contents.
// Favor original contents if image unchanged.
if (resized_ && opencv_image_ != NULL) {
cvSaveImage(opencv_filename_.c_str(), opencv_image_);
ok = file_system_->ReadFile(opencv_filename_.c_str(),
&opencv_contents, handler_);
file_system_->RemoveFile(opencv_filename_.c_str(), handler_);
if (ok) {
contents = opencv_contents;
}
}
// Take image contents and re-compress them.
if (ok) {
// If we can't optimize the image, we'll fail.
ok = false;
switch (image_type()) {
case IMAGE_UNKNOWN:
break;
case IMAGE_JPEG:
// TODO(jmarantz): The PageSpeed library should, ideally, take
// StringPiece args rather than const string&. We would save
// lots of string-copying if we made that change.
ok = pagespeed::image_compression::OptimizeJpeg(
std::string(contents.data(), contents.size()),
&output_contents_);
break;
case IMAGE_PNG: {
pagespeed::image_compression::PngReader png_reader;
ok = pagespeed::image_compression::PngOptimizer::OptimizePng(
png_reader,
std::string(contents.data(), contents.size()),
&output_contents_);
break;
}
case IMAGE_GIF: {
#if PAGESPEED_PNG_OPTIMIZER_GIF_READER
pagespeed::image_compression::GifReader gif_reader;
ok = pagespeed::image_compression::PngOptimizer::OptimizePng(
gif_reader, contents, &output_contents_);
if (ok) {
image_type_ = IMAGE_PNG;
}
#endif
break;
}
}
}
output_valid_ = ok;
}
return output_valid_;
}
StringPiece Image::Contents() {
StringPiece contents;
if (this->image_type() != IMAGE_UNKNOWN) {
contents = original_contents_;
if (output_valid_ || ComputeOutputContents()) {
contents = output_contents_;
}
}
return contents;
}
} // namespace net_instaweb