blob: c1e78698fd443a5a0f4d3f87f2b58c61f0478039 [file] [log] [blame]
// Copyright 2019 Google LLC
//
// 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
//
// https://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.
// -----------------------------------------------------------------------------
//
// PNG decode.
//
// Author: Skal (pascal.massimino@gmail.com)
#include <cstdio>
#include <cstdlib>
#ifdef HAVE_CONFIG_H
#include "src/wp2/config.h"
#endif
#include "imageio/anim_image_dec.h"
#include "imageio/imageio_util.h"
#include "src/wp2/base.h"
#ifdef WP2_HAVE_PNG
#ifndef PNG_USER_MEM_SUPPORTED
#define PNG_USER_MEM_SUPPORTED // for png_create_read_struct_2
#endif
#include <png.h>
#include <pngconf.h>
#include <pnglibconf.h>
#include <setjmp.h> // note: this must be included *after* png.h
#include <cstdint>
#include <cstring>
#include "src/utils/utils.h"
#define LOCAL_PNG_VERSION ((PNG_LIBPNG_VER_MAJOR << 8) | PNG_LIBPNG_VER_MINOR)
#define LOCAL_PNG_PREREQ(maj, min) (LOCAL_PNG_VERSION >= (((maj) << 8) | (min)))
#if LOCAL_PNG_PREREQ(1, 4)
typedef png_alloc_size_t LocalPngAllocSize;
#else
typedef png_size_t LocalPngAllocSize;
#endif
namespace {
//------------------------------------------------------------------------------
void PNGAPI ErrorFunctionNoLog(png_structp png, png_const_charp error) {
(void)error;
longjmp(png_jmpbuf(png), 1);
}
void PNGAPI ErrorFunction(png_structp png, png_const_charp error) {
if (error != NULL) {
fprintf(stderr, "libpng error: %s\n", error);
} else {
fprintf(stderr, "unknown libpng error\n");
}
ErrorFunctionNoLog(png, error);
}
png_voidp PNGAPI MallocFunction(png_structp png_ptr, LocalPngAllocSize size) {
const png_voidp mem = (png_voidp)WP2Malloc(1, (size_t)size);
if (mem == nullptr) {
volatile bool* const had_malloc_error =
(volatile bool*)png_get_mem_ptr(png_ptr);
if (had_malloc_error != NULL) *had_malloc_error = true;
}
return mem;
}
void PNGAPI FreeFunction(png_structp png_ptr, png_voidp ptr) {
(void)png_ptr;
WP2Free(ptr);
}
//------------------------------------------------------------------------------
// Converts the NULL terminated 'hexstring' which contains 2-byte character
// representations of hex values to raw data.
// 'hexstring' may contain values consisting of [A-F][a-f][0-9] in pairs,
// e.g., 7af2..., separated by any number of newlines.
// 'expected_length' is the anticipated processed size.
// On success the 'payload' is filled with its length equivalent to
// 'expected_length' and WP2_STATUS_OK is returned. An error is returned if the
// allocation failed, if the processed length is less than 'expected_length' or
// if any character aside from those above is encountered.
WP2Status HexStringToBytes(const char* const hexstring, size_t expected_length,
WP2::Data* const payload) {
WP2_CHECK_STATUS(payload->Resize(expected_length, /*keep_bytes=*/false));
const char* src = hexstring;
size_t actual_length = 0;
for (uint8_t* dst = payload->bytes;
actual_length < expected_length && *src != '\0'; ++src) {
if (*src == '\n') continue;
const char val[3]{*src++, *src, '\0'}; // One hex byte string ("0F").
char* end;
*dst++ = (uint8_t)std::strtol(val, &end, 16); // strtol NOLINT
WP2_CHECK_OK(end == val + 2, WP2_STATUS_BITSTREAM_ERROR);
++actual_length;
}
WP2_CHECK_OK(actual_length == expected_length, WP2_STATUS_BITSTREAM_ERROR);
return WP2_STATUS_OK;
}
WP2Status ProcessRawProfile(const char* const profile, size_t profile_len,
WP2::Data* const payload, WP2::LogLevel log_level) {
WP2_CHECK_OK(profile != nullptr, WP2_STATUS_NULL_PARAMETER);
WP2_CHECK_OK(profile_len > 0, WP2_STATUS_NOT_ENOUGH_DATA);
// ImageMagick formats 'raw profiles' as
// '\n<name>\n<length>(%8lu)\n<hex payload>\n'.
const char* src = profile;
if (*src != '\n') {
if (log_level >= WP2::LogLevel::DEFAULT) {
fprintf(stderr, "Malformed raw profile, expected '\\n' got '\\x%.2X'\n",
*src);
}
return WP2_STATUS_BITSTREAM_ERROR;
}
++src;
// Skip the profile name and extract the length.
while (*src != '\0' && *src++ != '\n') {
}
char* end;
const size_t expected_length = (size_t)std::strtol(src, &end, 10); // NOLINT
if (*end != '\n') {
if (log_level >= WP2::LogLevel::DEFAULT) {
fprintf(stderr, "Malformed raw profile, expected '\\n' got '\\x%.2X'\n",
*end);
}
return WP2_STATUS_BITSTREAM_ERROR;
}
++end;
// 'end' now points to the profile payload.
WP2_CHECK_STATUS(HexStringToBytes(end, expected_length, payload));
return WP2_STATUS_OK;
}
WP2Status ProcessProfile(const char* const profile, size_t profile_len,
WP2::Data* const payload) {
WP2_CHECK_OK(profile != nullptr, WP2_STATUS_NULL_PARAMETER);
WP2_CHECK_OK(profile_len > 0, WP2_STATUS_NOT_ENOUGH_DATA);
return payload->CopyFrom((const uint8_t*)profile, profile_len);
}
//------------------------------------------------------------------------------
png_size_t GetTextLength(png_const_textp const text_chunk) {
#ifdef PNG_iTXt_SUPPORTED
if (text_chunk->compression == PNG_ITXT_COMPRESSION_NONE ||
text_chunk->compression == PNG_ITXT_COMPRESSION_zTXt) {
return text_chunk->itxt_length;
}
#endif
return text_chunk->text_length;
}
// Returns the associated 'metadata' payload and if it 'is_raw_profile' based on
// the PNG text chunk 'key'. Returns null if unhandled.
WP2::Data* GetPayload(const char* const key, WP2::Metadata* const metadata,
bool* const is_raw_profile) {
// www.sno.phy.queensu.ca/~phil/exiftool/TagNames/PNG.html#TextualData
// See also: ExifTool on CPAN. Exiftool puts exif data in APP1 chunk, too.
if (!std::strcmp(key, "Raw profile type exif") ||
!std::strcmp(key, "Raw profile type APP1")) {
*is_raw_profile = true;
return &metadata->exif;
}
if (!std::strcmp(key, "Raw profile type xmp")) {
*is_raw_profile = true;
return &metadata->xmp;
}
// XMP Specification Part 3, Section 3 #PNG
if (!std::strcmp(key, "XML:com.adobe.xmp")) {
*is_raw_profile = false;
return &metadata->xmp;
}
return nullptr;
}
// Looks for metadata in 'info' and extracts it to 'metadata'.
// Returns WP2_STATUS_OK on success.
WP2Status ExtractMetadataFromPNG(png_const_structrp png, png_infop const info,
WP2::Metadata* const metadata,
WP2::LogLevel log_level) {
// Look for EXIF / XMP metadata.
png_textp text_chunk = NULL;
const int num_text_chunks = png_get_text(png, info, &text_chunk, NULL);
for (int i = 0; i < num_text_chunks; ++i, ++text_chunk) {
bool is_raw_profile = false;
WP2::Data* const payload =
GetPayload((const char*)text_chunk->key, metadata, &is_raw_profile);
if (payload == nullptr) {
if (log_level >= WP2::LogLevel::VERBOSE) {
fprintf(stderr, "Ignoring unhandled '%s'\n", text_chunk->key);
}
} else if (!payload->IsEmpty()) {
if (log_level >= WP2::LogLevel::VERBOSE) {
fprintf(stderr, "Ignoring additional '%s'\n", text_chunk->key);
}
} else {
const png_size_t text_length = GetTextLength(text_chunk);
WP2Status status;
if (is_raw_profile) {
status = ProcessRawProfile(text_chunk->text, text_length, payload,
log_level);
} else {
status = ProcessProfile(text_chunk->text, text_length, payload);
}
if (status != WP2_STATUS_OK) {
if (log_level >= WP2::LogLevel::DEFAULT) {
fprintf(stderr, "Failed to process: '%s'\n", text_chunk->key);
fprintf(stderr, "Error extracting PNG metadata!\n");
}
return status;
}
}
}
if (metadata->iccp.IsEmpty()) {
// Look for an ICC profile.
#if LOCAL_PNG_PREREQ(1, 5)
png_bytep profile;
#else // < libpng 1.5.0
png_charp profile;
#endif
png_uint_32 length;
png_charp name;
int comp_type;
if (png_get_iCCP(png, info, &name, &comp_type, &profile, &length) ==
PNG_INFO_iCCP) {
const WP2Status status =
ProcessProfile((const char*)profile, length, &metadata->iccp);
if (status != WP2_STATUS_OK) {
if (log_level >= WP2::LogLevel::DEFAULT) {
fprintf(stderr, "Failed to process: '%s'\n", text_chunk->key);
fprintf(stderr, "Error extracting PNG metadata!\n");
}
return status;
}
}
}
return WP2_STATUS_OK;
}
//------------------------------------------------------------------------------
struct PNGReadContext {
const uint8_t* data;
size_t data_size;
png_size_t offset;
};
void ReadFunc(png_structp png_ptr, png_bytep data, png_size_t length) {
PNGReadContext* const ctx = (PNGReadContext*)png_get_io_ptr(png_ptr);
if (ctx->data_size - ctx->offset < length) {
png_error(png_ptr, "ReadFunc: invalid read length (overflow)!");
}
std::memcpy(data, ctx->data + ctx->offset, length);
ctx->offset += length;
}
} // namespace
// -----------------------------------------------------------------------------
namespace WP2 {
class ImageReaderPNG : public ImageReader::Impl {
public:
ImageReaderPNG(const uint8_t* data, size_t data_size,
ArgbBuffer* const buffer, LogLevel log_level,
size_t max_num_pixels)
: ImageReader::Impl(buffer, data, data_size, log_level, max_num_pixels) {}
WP2Status ReadFrame(bool* const is_last,
uint32_t* const duration_ms) override {
WP2_CHECK_STATUS(CheckData());
// The following are volatile because they need to keep their value for
// deletion in case of longjmp() error handling.
png_infop volatile head_info = NULL;
png_infop volatile end_info = NULL;
tmp_rgb_ = nullptr;
had_malloc_error_ = false;
volatile png_error_ptr error_function =
(log_level_ >= LogLevel::DEFAULT) ? ErrorFunction : ErrorFunctionNoLog;
volatile png_structp const png = png_create_read_struct_2(
PNG_LIBPNG_VER_STRING, NULL, error_function, NULL,
(png_voidp)&had_malloc_error_, MallocFunction, FreeFunction);
WP2_CHECK_ALLOC_OK(png != NULL);
if (setjmp(png_jmpbuf(png))) {
// On a libpng error, error_function() is called, longjmping here.
png_destroy_read_struct((png_structpp)&png, (png_infopp)&head_info,
(png_infopp)&end_info);
WP2Free(tmp_rgb_);
tmp_rgb_ = nullptr; // for good measure
return had_malloc_error_ ? WP2_STATUS_OUT_OF_MEMORY
: WP2_STATUS_BITSTREAM_ERROR;
}
#if LOCAL_PNG_PREREQ(1, 5) || \
(LOCAL_PNG_PREREQ(1, 4) && PNG_LIBPNG_VER_RELEASE >= 1)
// If it looks like the bitstream is going to need more memory than libpng's
// internal limit (default: 8M), try to (reasonably) raise it.
// The WP2_MAX_ALLOCABLE_MEMORY limit is probably too high, use lower limit.
const size_t kMaxPNGMemory = (1u << 24);
if (data_size_ > png_get_chunk_malloc_max(png) &&
data_size_ <= kMaxPNGMemory) {
if (log_level_ >= WP2::LogLevel::DEFAULT) {
fprintf(stderr, "PNGDec: raising memory limit.\n");
}
png_set_chunk_malloc_max(png, data_size_);
}
#endif
if ((head_info = png_create_info_struct(png)) == NULL ||
(end_info = png_create_info_struct(png)) == NULL) {
png_destroy_read_struct((png_structpp)&png, (png_infopp)&head_info,
(png_infopp)&end_info);
return WP2_STATUS_OUT_OF_MEMORY;
}
PNGReadContext context = {data_, data_size_, 0};
png_set_read_fn(png, &context, ReadFunc);
png_read_info(png, head_info);
WP2Status status = ReadCanvas(png, head_info);
WP2Free(tmp_rgb_);
tmp_rgb_ = nullptr;
// Look for metadata at both the beginning and the end of the PNG file,
// giving preference to the head.
if (status == WP2_STATUS_OK) {
status = ExtractMetadataFromPNG(png, head_info, &buffer_->metadata_,
log_level_);
}
if (status == WP2_STATUS_OK) {
png_read_end(png, end_info);
status = ExtractMetadataFromPNG(png, end_info, &buffer_->metadata_,
log_level_);
}
png_destroy_read_struct((png_structpp)&png, (png_infopp)&head_info,
(png_infopp)&end_info);
if (status == WP2_STATUS_OK) {
*is_last = true;
*duration_ms = ImageReader::kInfiniteDuration;
}
return status;
}
protected:
// Reads pixels into 'buffer_'. Allocates 'tmp_rgb_'.
WP2Status ReadCanvas(png_structp const png, png_inforp const head_info) {
int color_type, bit_depth, interlaced;
png_uint_32 width, height;
WP2_CHECK_OK(png_get_IHDR(png, head_info, &width, &height, &bit_depth,
&color_type, &interlaced, NULL, NULL),
WP2_STATUS_BITSTREAM_ERROR);
WP2_CHECK_STATUS(CheckDimensions((uint32_t)width, (uint32_t)height));
png_set_strip_16(png);
png_set_packing(png);
if (color_type == PNG_COLOR_TYPE_PALETTE) {
png_set_palette_to_rgb(png);
} else if (color_type == PNG_COLOR_TYPE_GRAY ||
color_type == PNG_COLOR_TYPE_GRAY_ALPHA) {
if (bit_depth < 8) png_set_expand_gray_1_2_4_to_8(png);
png_set_gray_to_rgb(png);
}
if (png_get_valid(png, head_info, PNG_INFO_tRNS)) {
png_set_tRNS_to_alpha(png);
}
// Apply gamma correction if needed.
double image_gamma = 1 / 2.2;
if (png_get_gAMA(png, head_info, &image_gamma)) {
const double screen_gamma = 2.2;
png_set_gamma(png, screen_gamma, image_gamma);
}
const int num_passes = png_set_interlace_handling(png);
png_read_update_info(png, head_info);
const uint32_t num_channels = png_get_channels(png, head_info);
if (num_channels != 3 && num_channels != 4) {
return WP2_STATUS_BITSTREAM_ERROR;
}
const uint32_t depth = num_channels * sizeof(*tmp_rgb_);
size_t stride = 0;
WP2_CHECK_STATUS(MultFitsIn(width, depth, &stride));
// Make sure whole size fits in size_t.
WP2_CHECK_STATUS(MultFitsIn<size_t>(stride, height));
WP2_CHECK_STATUS(buffer_->Resize((uint32_t)width, (uint32_t)height));
// For interlaced PNG, libpng needs to retain the full image in memory.
tmp_rgb_ = (uint8_t*)WP2Malloc(num_passes > 1 ? height : 1, stride);
WP2_CHECK_ALLOC_OK(tmp_rgb_ != nullptr);
for (int p = 0; p < num_passes; ++p) {
png_bytep row = tmp_rgb_;
for (uint32_t y = 0; y < height; ++y) {
png_read_row(png, row, NULL);
if (p == num_passes - 1) {
WP2_CHECK_STATUS(buffer_->ImportRow(
(num_channels == 4) ? WP2_RGBA_32 : WP2_RGB_24, y, row));
}
if (num_passes > 1) row += stride;
}
}
return WP2_STATUS_OK;
}
private:
// Easier to store it here than as an argument of ReadCanvas().
// Volatile because allocation happens between setjmp() and longjmp() and
// needs to be freed afterwards.
uint8_t* volatile tmp_rgb_;
volatile bool had_malloc_error_;
};
// -----------------------------------------------------------------------------
void ImageReader::SetImplPNG(ArgbBuffer* const buffer, LogLevel log_level,
size_t max_num_pixels) {
impl_.reset(new (WP2Allocable::nothrow) ImageReaderPNG(
data_.bytes, data_.size, buffer, log_level, max_num_pixels));
if (impl_ == nullptr) status_ = WP2_STATUS_OUT_OF_MEMORY;
}
} // namespace WP2
#else // !WP2_HAVE_PNG
void WP2::ImageReader::SetImplPNG(WP2::ArgbBuffer* const buffer,
WP2::LogLevel log_level,
size_t max_num_pixels) {
(void)buffer;
(void)max_num_pixels;
if (log_level >= WP2::LogLevel::DEFAULT) {
fprintf(stderr,
"PNG support not compiled. Please install the libpng "
"development package before building.\n");
}
status_ = WP2_STATUS_UNSUPPORTED_FEATURE;
}
#endif // WP2_HAVE_PNG