From 274daf541551690fefa8d27cd2b99caded5c3750 Mon Sep 17 00:00:00 2001 From: Pascal Massimino Date: Tue, 14 Mar 2017 10:00:43 +0100 Subject: [PATCH] Add webpinfo tool. A command line tool to print out the chunk level structure of WebP files along with basic integrity checks. BUG=webp:330 Change-Id: Ic69f646f649abb655b1854621d99afedeed158d7 --- CMakeLists.txt | 1 + Makefile.vc | 5 +- build.gradle | 18 ++ configure.ac | 6 + examples/Android.mk | 16 + examples/Makefile.am | 8 + examples/webpinfo.c | 754 +++++++++++++++++++++++++++++++++++++++++++ makefile.unix | 5 +- 8 files changed, 811 insertions(+), 2 deletions(-) create mode 100644 examples/webpinfo.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 977e7374..6986f9b4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -8,6 +8,7 @@ option(WEBP_BUILD_CWEBP "Build the cwebp command line tool." OFF) option(WEBP_BUILD_DWEBP "Build the dwebp command line tool." OFF) option(WEBP_BUILD_GIF2WEBP "Build the gif2webp conversion tool." OFF) option(WEBP_BUILD_IMG2WEBP "Build the img2webp animation tool." OFF) +option(WEBP_BUILD_WEBPINFO "Build the webpinfo command line tool." OFF) option(WEBP_BUILD_WEBP_JS "Emscripten build of webp.js." OFF) option(WEBP_EXPERIMENTAL_FEATURES "Build with experimental features." OFF) option(WEBP_ENABLE_SWAP_16BIT_CSP "Enable byte swap for 16 bit colorspaces." OFF) diff --git a/Makefile.vc b/Makefile.vc index c04a43c0..33ed6963 100644 --- a/Makefile.vc +++ b/Makefile.vc @@ -348,6 +348,7 @@ OUT_EXAMPLES = $(DIRBIN)\cwebp.exe $(DIRBIN)\dwebp.exe EXTRA_EXAMPLES = $(DIRBIN)\vwebp.exe $(DIRBIN)\webpmux.exe \ $(DIRBIN)\img2webp.exe $(DIRBIN)\get_disto.exe \ $(DIRBIN)\webp_quality.exe $(DIRBIN)\vwebp_sdl.exe \ + $(DIRBIN)\webpinfo.exe ex: $(OUT_LIBS) $(OUT_EXAMPLES) all: ex $(EXTRA_EXAMPLES) @@ -379,10 +380,12 @@ $(DIRBIN)\img2webp.exe: $(IMAGEIO_DEC_OBJS) $(DIRBIN)\img2webp.exe: $(EX_UTIL_OBJS) $(IMAGEIO_UTIL_OBJS) $(LIBWEBP) $(DIRBIN)\get_disto.exe: $(DIROBJ)\extras\get_disto.obj $(DIRBIN)\get_disto.exe: $(IMAGEIO_DEC_OBJS) $(IMAGEIO_UTIL_OBJS) $(LIBWEBP) - $(DIRBIN)\webp_quality.exe: $(DIROBJ)\extras\webp_quality.obj $(DIRBIN)\webp_quality.exe: $(IMAGEIO_UTIL_OBJS) $(DIRBIN)\webp_quality.exe: $(EXTRAS_OBJS) $(LIBWEBP) +$(DIRBIN)\webpinfo.exe: $(DIROBJ)\examples\webpinfo.obj +$(DIRBIN)\webpinfo.exe: $(IMAGEIO_DEC_OBJS) +$(DIRBIN)\webpinfo.exe: $(EX_UTIL_OBJS) $(IMAGEIO_UTIL_OBJS) $(LIBWEBP) $(OUT_EXAMPLES): $(EX_UTIL_OBJS) $(LIBWEBP) $(EX_UTIL_OBJS) $(IMAGEIO_UTIL_OBJS): $(OUTPUT_DIRS) diff --git a/build.gradle b/build.gradle index 1af8bf37..81fd74de 100644 --- a/build.gradle +++ b/build.gradle @@ -392,6 +392,24 @@ model { } } } + + webpinfo_example(NativeExecutableSpec) { + binaries { + all { + lib library: "example_util", linkage: "static" + lib library: "imageio_util", linkage: "static" + lib library: "webp" + } + } + sources { + c { + source { + srcDir "./examples" + include "webpinfo.c" + } + } + } + } } tasks { // Task to test all possible configurations. diff --git a/configure.ac b/configure.ac index 0460af33..a68e69f8 100644 --- a/configure.ac +++ b/configure.ac @@ -599,6 +599,11 @@ if test "$enable_libwebpmux" = "yes"; then fi AM_CONDITIONAL([BUILD_IMG2WEBP], [test "${build_img2webp}" = "yes"]) +if test "$enable_libwebpmux" = "yes"; then + build_webpinfo=yes +fi +AM_CONDITIONAL([BUILD_WEBPINFO], [test "${build_webpinfo}" = "yes"]) + dnl === check for WIC support === AC_ARG_ENABLE([wic], @@ -756,6 +761,7 @@ gif2webp : ${build_gif2webp-no} img2webp : ${build_img2webp-no} webpmux : ${enable_libwebpmux-no} vwebp : ${build_vwebp-no} +webpinfo : ${build_webpinfo-no} SDL support : ${sdl_support-no} vwebp_sdl : ${build_vwebp_sdl-no} ]) diff --git a/examples/Android.mk b/examples/Android.mk index 00d53baf..dd7364f5 100644 --- a/examples/Android.mk +++ b/examples/Android.mk @@ -80,3 +80,19 @@ LOCAL_STATIC_LIBRARIES := example_util imageio_util imagedec webpmux webp LOCAL_MODULE := img2webp_example include $(BUILD_EXECUTABLE) + +################################################################################ +# webpinfo + +include $(CLEAR_VARS) + +LOCAL_SRC_FILES := \ + webpinfo.c \ + +LOCAL_CFLAGS := $(WEBP_CFLAGS) +LOCAL_C_INCLUDES := $(LOCAL_PATH)/../src +LOCAL_STATIC_LIBRARIES := example_util imageio_util webp + +LOCAL_MODULE := webpinfo_example + +include $(BUILD_EXECUTABLE) diff --git a/examples/Makefile.am b/examples/Makefile.am index 4422a398..37c52798 100644 --- a/examples/Makefile.am +++ b/examples/Makefile.am @@ -16,6 +16,9 @@ endif if BUILD_VWEBP bin_PROGRAMS += vwebp endif +if BUILD_WEBPINFO + bin_PROGRAMS += webpinfo +endif noinst_LTLIBRARIES = libexample_util.la @@ -66,6 +69,11 @@ img2webp_LDADD += ../imageio/libimagedec.la img2webp_LDADD += ../src/mux/libwebpmux.la ../src/libwebp.la img2webp_LDADD += $(PNG_LIBS) $(JPEG_LIBS) $(TIFF_LIBS) +webpinfo_SOURCES = webpinfo.c +webpinfo_CPPFLAGS = $(AM_CPPFLAGS) $(USE_EXPERIMENTAL_CODE) +webpinfo_LDADD = libexample_util.la ../imageio/libimageio_util.la +webpinfo_LDADD += ../src/libwebp.la + if BUILD_LIBWEBPDECODER anim_diff_LDADD += ../src/libwebpdecoder.la vwebp_LDADD += ../src/libwebpdecoder.la diff --git a/examples/webpinfo.c b/examples/webpinfo.c new file mode 100644 index 00000000..8187ab66 --- /dev/null +++ b/examples/webpinfo.c @@ -0,0 +1,754 @@ +// Copyright 2017 Google Inc. All Rights Reserved. +// +// Use of this source code is governed by a BSD-style license +// that can be found in the COPYING file in the root of the source +// tree. An additional intellectual property rights grant can be found +// in the file PATENTS. All contributing project authors may +// be found in the AUTHORS file in the root of the source tree. +// ----------------------------------------------------------------------------- +// +// Command-line tool to print out the chunk level structure of WebP files +// along with basic integrity checks. +// +// Author: Hui Su (huisu@google.com) + +#include +#include + +#ifdef HAVE_CONFIG_H +#include "webp/config.h" +#endif + +#include "../imageio/imageio_util.h" +#include "webp/decode.h" +#include "webp/format_constants.h" +#include "webp/mux_types.h" + +#if defined(_MSC_VER) && _MSC_VER < 1900 +#define snprintf _snprintf +#endif + +typedef enum { + WEBP_INFO_OK = 0, + WEBP_INFO_TRUNCATED_DATA, + WEBP_INFO_PARSE_ERROR, + WEBP_INFO_INVALID_PARAM, + WEBP_INFO_BITSTREAM_ERROR, + WEBP_INFO_MISSING_DATA, + WEBP_INFO_INVALID_COMMAND +} WebPInfoStatus; + +typedef enum ChunkID { + CHUNK_VP8, + CHUNK_VP8L, + CHUNK_VP8X, + CHUNK_ALPHA, + CHUNK_ANIM, + CHUNK_ANMF, + CHUNK_ICCP, + CHUNK_EXIF, + CHUNK_XMP, + CHUNK_UNKNOWN, + CHUNK_TYPES = CHUNK_UNKNOWN +} ChunkID; + +typedef struct { + size_t start_; + size_t end_; + const uint8_t* buf_; +} MemBuffer; + +typedef struct { + size_t offset_; + size_t size_; + const uint8_t* payload_; + ChunkID id_; +} ChunkData; + +typedef struct WebPInfo { + int canvas_width_; + int canvas_height_; + int loop_count_; + int num_frames_; + int chunk_counts_[CHUNK_TYPES]; + int anmf_subchunk_counts_[3]; // 0 VP8; 1 VP8L; 2 ALPH. + uint32_t bgcolor_; + int feature_flags_; + int has_alpha_; + // Used for parsing ANMF chunks. + int frame_width_, frame_height_; + size_t anim_frame_data_size_; + int is_processing_anim_frame_, seen_alpha_subchunk_, seen_image_subchunk_; + // Print output control. + int quiet_, show_diagnosis_, show_summary_; +} WebPInfo; + +static void WebPInfoInit(WebPInfo* const webp_info) { + memset(webp_info, 0, sizeof(*webp_info)); +} + +static const char kWebPChunkTags[CHUNK_TYPES][4] = { + { 'V', 'P', '8', ' ' }, + { 'V', 'P', '8', 'L' }, + { 'V', 'P', '8', 'X' }, + { 'A', 'L', 'P', 'H' }, + { 'A', 'N', 'I', 'M' }, + { 'A', 'N', 'M', 'F' }, + { 'I', 'C', 'C', 'P' }, + { 'E', 'X', 'I', 'F' }, + { 'X', 'M', 'P', ' ' }, +}; + +// ----------------------------------------------------------------------------- +// Data reading. + +static int GetLE16(const uint8_t* const data) { + return (data[0] << 0) | (data[1] << 8); +} + +static int GetLE24(const uint8_t* const data) { + return GetLE16(data) | (data[2] << 16); +} + +static uint32_t GetLE32(const uint8_t* const data) { + return GetLE16(data) | ((uint32_t)GetLE16(data + 2) << 16); +} + +static int ReadLE16(const uint8_t** data) { + const int val = GetLE16(*data); + *data += 2; + return val; +} + +static int ReadLE24(const uint8_t** data) { + const int val = GetLE24(*data); + *data += 3; + return val; +} + +static uint32_t ReadLE32(const uint8_t** data) { + const uint32_t val = GetLE32(*data); + *data += 4; + return val; +} + +static int ReadFileToWebPData(const char* const filename, + WebPData* const webp_data) { + const uint8_t* data; + size_t size; + if (!ImgIoUtilReadFile(filename, &data, &size)) return 0; + webp_data->bytes = data; + webp_data->size = size; + return 1; +} + +// ----------------------------------------------------------------------------- +// MemBuffer object. + +static void InitMemBuffer(MemBuffer* const mem, const WebPData* webp_data) { + mem->buf_ = webp_data->bytes; + mem->start_ = 0; + mem->end_ = webp_data->size; +} + +static size_t MemDataSize(const MemBuffer* const mem) { + return (mem->end_ - mem->start_); +} + +static const uint8_t* GetBuffer(MemBuffer* const mem) { + return mem->buf_ + mem->start_; +} + +static void Skip(MemBuffer* const mem, size_t size) { + mem->start_ += size; +} + +static uint32_t ReadMemBufLE32(MemBuffer* const mem) { + const uint8_t* const data = mem->buf_ + mem->start_; + const uint32_t val = GetLE32(data); + assert(MemDataSize(mem) >= 4); + Skip(mem, 4); + return val; +} + +// ----------------------------------------------------------------------------- +// Chunk parsing. + +#define LOG_ERROR(MESSAGE) \ + if (webp_info->show_diagnosis_) { \ + fprintf(stderr, "Error: %s\n", MESSAGE); \ + } + +#define LOG_WARN(MESSAGE) \ + if (webp_info->show_diagnosis_) { \ + fprintf(stderr, "Warning: %s\n", MESSAGE); \ + } + +static WebPInfoStatus ParseRIFFHeader(const WebPInfo* const webp_info, + MemBuffer* const mem) { + const size_t min_size = RIFF_HEADER_SIZE + CHUNK_HEADER_SIZE; + size_t riff_size; + + if (MemDataSize(mem) < min_size) { + LOG_ERROR("Truncated data detected when parsing RIFF header."); + return WEBP_INFO_TRUNCATED_DATA; + } + if (memcmp(GetBuffer(mem), "RIFF", CHUNK_SIZE_BYTES) || + memcmp(GetBuffer(mem) + CHUNK_HEADER_SIZE, "WEBP", CHUNK_SIZE_BYTES)) { + LOG_ERROR("Corrupted RIFF header."); + return WEBP_INFO_PARSE_ERROR; + } + riff_size = GetLE32(GetBuffer(mem) + TAG_SIZE); + if (riff_size < CHUNK_HEADER_SIZE) { + LOG_ERROR("RIFF size is too small."); + return WEBP_INFO_PARSE_ERROR; + } + if (riff_size > MAX_CHUNK_PAYLOAD) { + LOG_ERROR("RIFF size is over limit."); + return WEBP_INFO_PARSE_ERROR; + } + riff_size += CHUNK_HEADER_SIZE; + if (!webp_info->quiet_) { + printf("RIFF HEADER:\n"); + printf(" RIFF file size: %6d\n", (int)riff_size); + } + if (riff_size < mem->end_) { + LOG_WARN("RIFF size is smaller than the file size."); + mem->end_ = riff_size; + } else if (riff_size > mem->end_) { + LOG_ERROR("Truncated data detected when parsing RIFF payload."); + return WEBP_INFO_TRUNCATED_DATA; + } + Skip(mem, RIFF_HEADER_SIZE); + return WEBP_INFO_OK; +} + +static WebPInfoStatus ParseChunk(const WebPInfo* const webp_info, + MemBuffer* const mem, + ChunkData* const chunk_data) { + memset(chunk_data, 0, sizeof(*chunk_data)); + if (MemDataSize(mem) < CHUNK_HEADER_SIZE) { + LOG_ERROR("Truncated data detected when parsing chunk header."); + return WEBP_INFO_TRUNCATED_DATA; + } else { + const size_t chunk_start_offset = mem->start_; + const uint32_t fourcc = ReadMemBufLE32(mem); + const uint32_t payload_size = ReadMemBufLE32(mem); + const uint32_t payload_size_padded = payload_size + (payload_size & 1); + const size_t chunk_size = CHUNK_HEADER_SIZE + payload_size_padded; + int i; + if (payload_size > MAX_CHUNK_PAYLOAD) { + LOG_ERROR("Size of chunk payload is over limit."); + return WEBP_INFO_INVALID_PARAM; + } + if (payload_size_padded > MemDataSize(mem)){ + LOG_ERROR("Truncated data detected when parsing chunk payload."); + return WEBP_INFO_TRUNCATED_DATA; + } + for (i = 0; i < CHUNK_TYPES; ++i) { + if (!memcmp(kWebPChunkTags[i], &fourcc, TAG_SIZE)) break; + } + chunk_data->offset_ = chunk_start_offset; + chunk_data->size_ = chunk_size; + chunk_data->id_ = (ChunkID)i; + chunk_data->payload_ = GetBuffer(mem); + if (chunk_data->id_ == CHUNK_ANMF) { + if (payload_size != payload_size_padded) { + LOG_ERROR("ANMF chunk size should always be even."); + return WEBP_INFO_PARSE_ERROR; + } + // There are sub-chunks to be parsed in an ANMF chunk. + Skip(mem, ANMF_CHUNK_SIZE); + } else { + Skip(mem, payload_size_padded); + } + return WEBP_INFO_OK; + } +} + +// ----------------------------------------------------------------------------- +// Chunk analysis. + +static WebPInfoStatus ProcessVP8XChunk(const ChunkData* const chunk_data, + WebPInfo* const webp_info) { + const uint8_t* data = chunk_data->payload_; + if (webp_info->chunk_counts_[CHUNK_VP8] || + webp_info->chunk_counts_[CHUNK_VP8L] || + webp_info->chunk_counts_[CHUNK_VP8X]) { + LOG_ERROR("Already seen a VP8/VP8L/VP8X chunk when parsing VP8X chunk."); + return WEBP_INFO_PARSE_ERROR; + } + if (chunk_data->size_ != VP8X_CHUNK_SIZE + CHUNK_HEADER_SIZE) { + LOG_ERROR("Corrupted VP8X chunk."); + return WEBP_INFO_PARSE_ERROR; + } + ++webp_info->chunk_counts_[CHUNK_VP8X]; + webp_info->feature_flags_ = *data; + data += 4; + webp_info->canvas_width_ = 1 + ReadLE24(&data); + webp_info->canvas_height_ = 1 + ReadLE24(&data); + if (!webp_info->quiet_) { + printf(" ICCP %d\n Alpha %d\n EXIF %d\n XMP %d\n Animation %d\n", + (webp_info->feature_flags_ & ICCP_FLAG) != 0, + (webp_info->feature_flags_ & ALPHA_FLAG) != 0, + (webp_info->feature_flags_ & EXIF_FLAG) != 0, + (webp_info->feature_flags_ & XMP_FLAG) != 0, + (webp_info->feature_flags_ & ANIMATION_FLAG) != 0); + printf(" Canvas size %d x %d\n", + webp_info->canvas_width_, webp_info->canvas_height_); + } + if (webp_info->canvas_width_ > MAX_CANVAS_SIZE) { + LOG_WARN("Canvas width is out of range in VP8X chunk."); + } + if (webp_info->canvas_height_ > MAX_CANVAS_SIZE) { + LOG_WARN("Canvas height is out of range in VP8X chunk."); + } + if ((uint64_t)webp_info->canvas_width_ * webp_info->canvas_height_ > + MAX_IMAGE_AREA) { + LOG_WARN("Canvas area is out of range in VP8X chunk."); + } + return WEBP_INFO_OK; +} + +static WebPInfoStatus ProcessANIMChunk(const ChunkData* const chunk_data, + WebPInfo* const webp_info) { + const uint8_t* data = chunk_data->payload_; + if (!webp_info->chunk_counts_[CHUNK_VP8X]) { + LOG_ERROR("ANIM chunk detected before VP8X chunk."); + return WEBP_INFO_PARSE_ERROR; + } + if (chunk_data->size_ != ANIM_CHUNK_SIZE + CHUNK_HEADER_SIZE) { + LOG_ERROR("Corrupted ANIM chunk."); + return WEBP_INFO_PARSE_ERROR; + } + webp_info->bgcolor_ = ReadLE32(&data); + webp_info->loop_count_ = ReadLE16(&data); + ++webp_info->chunk_counts_[CHUNK_ANIM]; + if (!webp_info->quiet_) { + printf(" Background color:(BGRA) %02x %02x %02x %02x\n", + (webp_info->bgcolor_ >> 24) & 0xff, + (webp_info->bgcolor_ >> 16) & 0xff, + (webp_info->bgcolor_ >> 8) & 0xff, + webp_info->bgcolor_ & 0xff); + printf(" Loop count : %d\n", webp_info->loop_count_); + } + if (webp_info->loop_count_ > MAX_LOOP_COUNT) { + LOG_WARN("Loop count is out of range in ANIM chunk."); + } + return WEBP_INFO_OK; +} + +static WebPInfoStatus ProcessANMFChunk(const ChunkData* const chunk_data, + WebPInfo* const webp_info) { + const uint8_t* data = chunk_data->payload_; + int offset_x, offset_y, width, height, duration, blend, dispose, temp; + if (webp_info->is_processing_anim_frame_) { + LOG_ERROR("ANMF chunk detected within another ANMF chunk."); + return WEBP_INFO_PARSE_ERROR; + } + if (!webp_info->chunk_counts_[CHUNK_ANIM]) { + LOG_ERROR("ANMF chunk detected before ANIM chunk."); + return WEBP_INFO_PARSE_ERROR; + } + if (chunk_data->size_ <= CHUNK_HEADER_SIZE + ANMF_CHUNK_SIZE) { + LOG_ERROR("Truncated data detected when parsing ANMF chunk."); + return WEBP_INFO_TRUNCATED_DATA; + } + offset_x = 2 * ReadLE24(&data); + offset_y = 2 * ReadLE24(&data); + width = 1 + ReadLE24(&data); + height = 1 + ReadLE24(&data); + duration = ReadLE24(&data); + temp = *data; + dispose = temp & 1; + blend = (temp >> 1) & 1; + ++webp_info->chunk_counts_[CHUNK_ANMF]; + if (!webp_info->quiet_) { + printf(" Offset_X %d\n Offset_Y %d\n Width %d\n Height %d\n" + " Duration %d\n Dispose %d\n Blend %d\n", + offset_x, offset_y, width, height, duration, dispose, blend); + } + if (duration > MAX_DURATION) { + LOG_ERROR("Invalid duration parameter in ANMF chunk."); + return WEBP_INFO_INVALID_PARAM; + } + if (offset_x > MAX_POSITION_OFFSET || offset_y > MAX_POSITION_OFFSET) { + LOG_ERROR("Invalid offset parameters in ANMF chunk."); + return WEBP_INFO_INVALID_PARAM; + } + if ((uint64_t)offset_x + width > (uint64_t)webp_info->canvas_width_ || + (uint64_t)offset_y + height > (uint64_t)webp_info->canvas_height_) { + LOG_ERROR("Frame exceeds canvas in ANMF chunk."); + return WEBP_INFO_INVALID_PARAM; + } + webp_info->is_processing_anim_frame_ = 1; + webp_info->seen_alpha_subchunk_ = 0; + webp_info->seen_image_subchunk_ = 0; + webp_info->frame_width_ = width; + webp_info->frame_height_ = height; + webp_info->anim_frame_data_size_ = + chunk_data->size_ - CHUNK_HEADER_SIZE - ANMF_CHUNK_SIZE; + return WEBP_INFO_OK; +} + +static WebPInfoStatus ProcessImageChunk(const ChunkData* const chunk_data, + WebPInfo* const webp_info) { + const uint8_t* data = chunk_data->payload_ - CHUNK_HEADER_SIZE; + WebPBitstreamFeatures features; + const VP8StatusCode vp8_status = + WebPGetFeatures(data, chunk_data->size_, &features); + if (vp8_status != VP8_STATUS_OK) { + LOG_ERROR("VP8/VP8L bitstream error."); + return WEBP_INFO_BITSTREAM_ERROR; + } + if (!webp_info->quiet_) { + printf(" Width %d\n Height %d\n Alpha %d\n Animation %d\n Format %d\n", + features.width, features.height, features.has_alpha, + features.has_animation, features.format); + } + if (webp_info->is_processing_anim_frame_) { + ++webp_info->anmf_subchunk_counts_[chunk_data->id_ == CHUNK_VP8 ? 0 : 1]; + if (chunk_data->id_ == CHUNK_VP8L && webp_info->seen_alpha_subchunk_) { + LOG_ERROR("Both VP8L and ALPH sub-chunks are present in an ANMF chunk."); + return WEBP_INFO_PARSE_ERROR; + } + if (webp_info->frame_width_ != features.width || + webp_info->frame_height_ != features.height) { + LOG_ERROR("Frame size in VP8/VP8L sub-chunk differs from ANMF header."); + return WEBP_INFO_PARSE_ERROR; + } + if (webp_info->seen_image_subchunk_) { + LOG_ERROR("Consecutive VP8/VP8L sub-chunks in an ANMF chunk."); + return WEBP_INFO_PARSE_ERROR; + } else { + webp_info->seen_image_subchunk_ = 1; + } + } else { + if (webp_info->chunk_counts_[CHUNK_VP8] || + webp_info->chunk_counts_[CHUNK_VP8L]) { + LOG_ERROR("Multiple VP8/VP8L chunks detected."); + return WEBP_INFO_PARSE_ERROR; + } + if (chunk_data->id_ == CHUNK_VP8L && + webp_info->chunk_counts_[CHUNK_ALPHA]) { + LOG_ERROR("Both VP8L and ALPH chunks are detected."); + return WEBP_INFO_PARSE_ERROR; + } + if (webp_info->chunk_counts_[CHUNK_ANIM] || + webp_info->chunk_counts_[CHUNK_ANMF]) { + LOG_ERROR("VP8/VP8L chunk and ANIM/ANMF chunk are both detected."); + return WEBP_INFO_PARSE_ERROR; + } + if (webp_info->chunk_counts_[CHUNK_VP8X]) { + if (webp_info->canvas_width_ != features.width || + webp_info->canvas_height_ != features.height) { + LOG_ERROR("Image size in VP8/VP8L chunk differs from VP8X chunk."); + return WEBP_INFO_PARSE_ERROR; + } + } else { + webp_info->canvas_width_ = features.width; + webp_info->canvas_height_ = features.height; + if (webp_info->canvas_width_ < 1 || webp_info->canvas_height_ < 1 || + webp_info->canvas_width_ > MAX_CANVAS_SIZE || + webp_info->canvas_height_ > MAX_CANVAS_SIZE || + (uint64_t)webp_info->canvas_width_ * webp_info->canvas_height_ > + MAX_IMAGE_AREA) { + LOG_WARN("Invalid parameters in VP8/VP8L chunk."); + } + } + ++webp_info->chunk_counts_[chunk_data->id_]; + } + ++webp_info->num_frames_; + webp_info->has_alpha_ |= features.has_alpha; + return WEBP_INFO_OK; +} + +static WebPInfoStatus ProcessALPHChunk(const ChunkData* const chunk_data, + WebPInfo* const webp_info) { + (void)chunk_data; + if (webp_info->is_processing_anim_frame_) { + ++webp_info->anmf_subchunk_counts_[2]; + if (webp_info->seen_alpha_subchunk_) { + LOG_ERROR("Consecutive ALPH sub-chunks in an ANMF chunk."); + return WEBP_INFO_PARSE_ERROR; + } else { + webp_info->seen_alpha_subchunk_ = 1; + } + if (webp_info->seen_image_subchunk_) { + LOG_ERROR("ALPHA sub-chunk detected after VP8 sub-chunk " + "in an ANMF chunk."); + return WEBP_INFO_PARSE_ERROR; + } + } else { + if (webp_info->chunk_counts_[CHUNK_ANIM] || + webp_info->chunk_counts_[CHUNK_ANMF]) { + LOG_ERROR("ALPHA chunk and ANIM/ANMF chunk are both detected."); + return WEBP_INFO_PARSE_ERROR; + } + if (!webp_info->chunk_counts_[CHUNK_VP8X]) { + LOG_ERROR("ALPHA chunk detected before VP8X chunk."); + return WEBP_INFO_PARSE_ERROR; + } + if (webp_info->chunk_counts_[CHUNK_VP8]) { + LOG_ERROR("ALPHA chunk detected after VP8 chunk."); + return WEBP_INFO_PARSE_ERROR; + } + if (webp_info->chunk_counts_[CHUNK_ALPHA]) { + LOG_ERROR("Multiple ALPHA chunks detected."); + return WEBP_INFO_PARSE_ERROR; + } + ++webp_info->chunk_counts_[CHUNK_ALPHA]; + } + webp_info->has_alpha_ = 1; + return WEBP_INFO_OK; +} + +static WebPInfoStatus ProcessICCPChunk(const ChunkData* const chunk_data, + WebPInfo* const webp_info) { + (void)chunk_data; + if (!webp_info->chunk_counts_[CHUNK_VP8X]) { + LOG_ERROR("ICCP chunk detected before VP8X chunk."); + return WEBP_INFO_PARSE_ERROR; + } + if (webp_info->chunk_counts_[CHUNK_VP8] || + webp_info->chunk_counts_[CHUNK_VP8L] || + webp_info->chunk_counts_[CHUNK_ANIM]) { + LOG_ERROR("ICCP chunk detected after image data."); + return WEBP_INFO_PARSE_ERROR; + } + ++webp_info->chunk_counts_[CHUNK_ICCP]; + return WEBP_INFO_OK; +} + +static WebPInfoStatus ProcessChunk(const ChunkData* const chunk_data, + WebPInfo* const webp_info) { + WebPInfoStatus status = WEBP_INFO_OK; + ChunkID id = chunk_data->id_; + if (chunk_data->id_ == CHUNK_UNKNOWN) { + char error_message[50]; + snprintf(error_message, 50, "Unknown chunk at offset %6d, length %6d", + (int)chunk_data->offset_, (int)chunk_data->size_); + LOG_WARN(error_message); + } else { + if (!webp_info->quiet_) { + const char* tag = kWebPChunkTags[chunk_data->id_]; + printf("Chunk %c%c%c%c at offset %6d, length %6d\n", + tag[0], tag[1], tag[2], tag[3], (int)chunk_data->offset_, + (int)chunk_data->size_); + } + } + switch (id) { + case CHUNK_VP8: + case CHUNK_VP8L: + status = ProcessImageChunk(chunk_data, webp_info); + break; + case CHUNK_VP8X: + status = ProcessVP8XChunk(chunk_data, webp_info); + break; + case CHUNK_ALPHA: + status = ProcessALPHChunk(chunk_data, webp_info); + break; + case CHUNK_ANIM: + status = ProcessANIMChunk(chunk_data, webp_info); + break; + case CHUNK_ANMF: + status = ProcessANMFChunk(chunk_data, webp_info); + break; + case CHUNK_ICCP: + status = ProcessICCPChunk(chunk_data, webp_info); + break; + case CHUNK_EXIF: + case CHUNK_XMP: + ++webp_info->chunk_counts_[id]; + break; + case CHUNK_UNKNOWN: + default: + break; + } + if (webp_info->is_processing_anim_frame_ && id != CHUNK_ANMF) { + if (webp_info->anim_frame_data_size_ == chunk_data->size_) { + if (!webp_info->seen_image_subchunk_) { + LOG_ERROR("No VP8/VP8L chunk detected in an ANMF chunk."); + return WEBP_INFO_PARSE_ERROR; + } + webp_info->is_processing_anim_frame_ = 0; + } else if (webp_info->anim_frame_data_size_ > chunk_data->size_) { + webp_info->anim_frame_data_size_ -= chunk_data->size_; + } else { + LOG_ERROR("Truncated data detected when parsing ANMF chunk."); + return WEBP_INFO_TRUNCATED_DATA; + } + } + return status; +} + +static WebPInfoStatus Validate(const WebPInfo* const webp_info) { + if (webp_info->num_frames_ < 1) { + LOG_ERROR("No image/frame detected."); + return WEBP_INFO_MISSING_DATA; + } + if (webp_info->chunk_counts_[CHUNK_VP8X]) { + const int iccp = !!(webp_info->feature_flags_ & ICCP_FLAG); + const int exif = !!(webp_info->feature_flags_ & EXIF_FLAG); + const int xmp = !!(webp_info->feature_flags_ & XMP_FLAG); + const int animation = !!(webp_info->feature_flags_ & ANIMATION_FLAG); + if (iccp && !webp_info->chunk_counts_[CHUNK_ICCP]) { + LOG_ERROR("Missing ICCP chunk."); + return WEBP_INFO_MISSING_DATA; + } + if (exif && !webp_info->chunk_counts_[CHUNK_EXIF]) { + LOG_ERROR("Missing EXIF chunk."); + return WEBP_INFO_MISSING_DATA; + } + if (xmp && !webp_info->chunk_counts_[CHUNK_XMP]) { + LOG_ERROR("Missing XMP chunk."); + return WEBP_INFO_MISSING_DATA; + } + if (!iccp && webp_info->chunk_counts_[CHUNK_ICCP]) { + LOG_ERROR("Unexpected ICCP chunk detected."); + return WEBP_INFO_PARSE_ERROR; + } + if (!exif && webp_info->chunk_counts_[CHUNK_EXIF]) { + LOG_ERROR("Unexpected EXIF chunk detected."); + return WEBP_INFO_PARSE_ERROR; + } + if (!xmp && webp_info->chunk_counts_[CHUNK_XMP]) { + LOG_ERROR("Unexpected XMP chunk detected."); + return WEBP_INFO_PARSE_ERROR; + } + // Incomplete animation frame. + if (webp_info->is_processing_anim_frame_) return WEBP_INFO_MISSING_DATA; + if (!animation && webp_info->num_frames_ > 1) { + LOG_ERROR("More than 1 frame detected in non-animation file."); + return WEBP_INFO_PARSE_ERROR; + } + if (animation && (!webp_info->chunk_counts_[CHUNK_ANIM] || + !webp_info->chunk_counts_[CHUNK_ANMF])) { + LOG_ERROR("No ANIM/ANMF chunk detected in animation file."); + return WEBP_INFO_PARSE_ERROR; + } + } + return WEBP_INFO_OK; +} + +static void ShowSummary(const WebPInfo* const webp_info) { + int i; + printf("Summary:\n"); + printf("Number of frames: %d\n", webp_info->num_frames_); + printf("Chunk type : VP8 VP8L VP8X ALPH ANIM ANMF(VP8 /VP8L/ALPH) ICCP " + "EXIF XMP\n"); + printf("Chunk counts: "); + for (i = 0; i < CHUNK_TYPES; ++i) { + printf("%4d ", webp_info->chunk_counts_[i]); + if (i == CHUNK_ANMF) { + printf("%4d %4d %4d ", + webp_info->anmf_subchunk_counts_[0], + webp_info->anmf_subchunk_counts_[1], + webp_info->anmf_subchunk_counts_[2]); + } + } + printf("\n"); +} + +static WebPInfoStatus AnalyzeWebP(WebPInfo* const webp_info, + const WebPData* webp_data) { + ChunkData chunk_data; + MemBuffer mem_buffer; + WebPInfoStatus webp_info_status = WEBP_INFO_OK; + + InitMemBuffer(&mem_buffer, webp_data); + webp_info_status = ParseRIFFHeader(webp_info, &mem_buffer); + if (webp_info_status != WEBP_INFO_OK) goto Error; + + // Loop through all the chunks. Terminate immediately in case of error. + while (webp_info_status == WEBP_INFO_OK && MemDataSize(&mem_buffer) > 0) { + webp_info_status = ParseChunk(webp_info, &mem_buffer, &chunk_data); + if (webp_info_status != WEBP_INFO_OK) goto Error; + webp_info_status = ProcessChunk(&chunk_data, webp_info); + } + if (webp_info_status != WEBP_INFO_OK) goto Error; + if (webp_info->show_summary_) ShowSummary(webp_info); + + // Final check. + webp_info_status = Validate(webp_info); + + Error: + if (!webp_info->quiet_) { + if (webp_info_status == WEBP_INFO_OK) { + printf("No error detected.\n"); + } else { + printf("Errors detected.\n"); + } + } + return webp_info_status; +} + +static void HelpShort(void) { + printf("Usage: webpinfo [options] in_files\n" + "Try -longhelp for an exhaustive list of options.\n"); +} + +static void HelpLong(void) { + printf("Usage: webpinfo [options] in_files\n" + "Note: there could be multiple input files;\n" + " options must come before input files.\n" + "Options:\n" + " -quiet ......... Do not show chunk parsing information.\n" + " -diag .......... Show parsing error diagnosis.\n" + " -summary ....... Show chunk stats summary.\n"); +} + +int main(int argc, const char* argv[]) { + int c, quiet = 0, show_diag = 0, show_summary = 0; + WebPInfoStatus webp_info_status = WEBP_INFO_OK; + WebPInfo webp_info; + + if (argc == 1) { + HelpShort(); + return WEBP_INFO_OK; + } + + // Parse command-line input. + for (c = 1; c < argc; ++c) { + if (!strcmp(argv[c], "-h") || !strcmp(argv[c], "-help")) { + HelpShort(); + return WEBP_INFO_OK; + } else if (!strcmp(argv[c], "-H") || !strcmp(argv[c], "-longhelp")) { + HelpLong(); + return WEBP_INFO_OK; + } else if (!strcmp(argv[c], "-quiet")) { + quiet = 1; + } else if (!strcmp(argv[c], "-diag")) { + show_diag = 1; + } else if (!strcmp(argv[c], "-summary")) { + show_summary = 1; + } else { // Assume the remaining are all input files. + break; + } + } + + if (c == argc) { + HelpShort(); + return WEBP_INFO_INVALID_COMMAND; + } + + // Process input files one by one. + for (; c < argc; ++c) { + WebPData webp_data; + const char* in_file = NULL; + WebPInfoInit(&webp_info); + webp_info.quiet_ = quiet; + webp_info.show_diagnosis_ = show_diag; + webp_info.show_summary_ = show_summary; + in_file = argv[c]; + if (in_file == NULL || !ReadFileToWebPData(in_file, &webp_data)) { + webp_info_status = WEBP_INFO_INVALID_COMMAND; + fprintf(stderr, "Failed to open input file %s.\n", in_file); + continue; + } + if (!webp_info.quiet_) printf("File %s.\n", in_file); + webp_info_status = AnalyzeWebP(&webp_info, &webp_data); + WebPDataClear(&webp_data); + } + return webp_info_status; +} diff --git a/makefile.unix b/makefile.unix index 2138e755..929f2e7a 100644 --- a/makefile.unix +++ b/makefile.unix @@ -333,7 +333,7 @@ OUT_LIBS += src/libwebp.a EXTRA_LIB = extras/libwebpextras.a OUT_EXAMPLES = examples/cwebp examples/dwebp EXTRA_EXAMPLES = examples/gif2webp examples/vwebp examples/webpmux \ - examples/anim_diff examples/img2webp + examples/anim_diff examples/img2webp examples/webpinfo OTHER_EXAMPLES = extras/get_disto extras/webp_quality extras/vwebp_sdl OUTPUT = $(OUT_LIBS) $(OUT_EXAMPLES) @@ -385,6 +385,7 @@ examples/gif2webp: examples/gif2webp.o $(GIFDEC_OBJS) examples/vwebp: examples/vwebp.o examples/webpmux: examples/webpmux.o examples/img2webp: examples/img2webp.o +examples/webpinfo: examples/webpinfo.o examples/anim_diff: examples/libanim_util.a examples/libgifdec.a examples/anim_diff: src/demux/libwebpdemux.a examples/libexample_util.a @@ -416,6 +417,8 @@ examples/img2webp: examples/libexample_util.a imageio/libimageio_util.a examples/img2webp: imageio/libimagedec.a examples/img2webp: src/mux/libwebpmux.a src/libwebp.a examples/img2webp: EXTRA_LIBS += $(CWEBP_LIBS) +examples/webpinfo: examples/libexample_util.a imageio/libimageio_util.a +examples/webpinfo: src/libwebpdecoder.a extras/get_disto: extras/get_disto.o extras/get_disto: imageio/libimagedec.a imageio/libimageio_util.a src/libwebp.a