// 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 #define LOG_ERROR(MESSAGE) \ do { \ if (webp_info->show_diagnosis_) { \ fprintf(stderr, "Error: %s\n", MESSAGE); \ } \ } while (0) #define LOG_WARN(MESSAGE) \ do { \ if (webp_info->show_diagnosis_) { \ fprintf(stderr, "Warning: %s\n", MESSAGE); \ } \ } while (0) static const char* const kFormats[3] = { "Unknown", "Lossy", "Lossless" }; static const char* const kLosslessTransforms[4] = { "Predictor", "Cross Color", "Subtract Green", "Color Indexing" }; static const char* const kAlphaFilterMethods[4] = { "None", "Horizontal", "Vertical", "Gradient" }; 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_; int parse_bitstream_; } 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; } // ----------------------------------------------------------------------------- // Lossy bitstream analysis. static int GetBits(const uint8_t* const data, size_t data_size, size_t nb, int* val, uint64_t* const bit_pos) { *val = 0; while (nb-- > 0) { const uint64_t p = (*bit_pos)++; if ((p >> 3) >= data_size) { return 0; } else { const int bit = !!(data[p >> 3] & (128 >> ((p & 7)))); *val = (*val << 1) | bit; } } return 1; } static int GetSignedBits(const uint8_t* const data, size_t data_size, size_t nb, int* val, uint64_t* const bit_pos) { int sign; if (!GetBits(data, data_size, nb, val, bit_pos)) return 0; if (!GetBits(data, data_size, 1, &sign, bit_pos)) return 0; if (sign) *val = -(*val); return 1; } #define GET_BITS(v, n) \ do { \ if (!GetBits(data, data_size, n, &v, bit_pos)) { \ LOG_ERROR("Truncated lossy bitstream."); \ return WEBP_INFO_TRUNCATED_DATA; \ } \ } while (0) #define GET_SIGNED_BITS(v, n) \ do { \ if (!GetSignedBits(data, data_size, n, &v, bit_pos)) { \ LOG_ERROR("Truncated lossy bitstream."); \ return WEBP_INFO_TRUNCATED_DATA; \ } \ } while (0) static WebPInfoStatus ParseLossySegmentHeader(const WebPInfo* const webp_info, const uint8_t* const data, size_t data_size, uint64_t* const bit_pos) { int use_segment; GET_BITS(use_segment, 1); printf(" Use segment: %d\n", use_segment); if (use_segment) { int update_map, update_data; GET_BITS(update_map, 1); GET_BITS(update_data, 1); printf(" Update map: %d\n" " Update data: %d\n", update_map, update_data); if (update_data) { int i, a_delta; int quantizer[4] = {0, 0, 0, 0}; int filter_strength[4] = {0, 0, 0, 0}; GET_BITS(a_delta, 1); printf(" Absolute delta: %d\n", a_delta); for (i = 0; i < 4; ++i) { int bit; GET_BITS(bit, 1); if (bit) GET_SIGNED_BITS(quantizer[i], 7); } for (i = 0; i < 4; ++i) { int bit; GET_BITS(bit, 1); if (bit) GET_SIGNED_BITS(filter_strength[i], 6); } printf(" Quantizer: %d %d %d %d\n", quantizer[0], quantizer[1], quantizer[2], quantizer[3]); printf(" Filter strength: %d %d %d %d\n", filter_strength[0], filter_strength[1], filter_strength[2], filter_strength[3]); } if (update_map) { int i; int prob_segment[3] = {255, 255, 255}; for (i = 0; i < 3; ++i) { int bit; GET_BITS(bit, 1); if (bit) GET_BITS(prob_segment[i], 8); } printf(" Prob segment: %d %d %d\n", prob_segment[0], prob_segment[1], prob_segment[2]); } } return WEBP_INFO_OK; } static WebPInfoStatus ParseLossyFilterHeader(const WebPInfo* const webp_info, const uint8_t* const data, size_t data_size, uint64_t* const bit_pos) { int simple_filter, level, sharpness, use_lf_delta; GET_BITS(simple_filter, 1); GET_BITS(level, 6); GET_BITS(sharpness, 3); GET_BITS(use_lf_delta, 1); printf(" Simple filter: %d\n", simple_filter); printf(" Level: %d\n", level); printf(" Sharpness: %d\n", sharpness); printf(" Use lf delta: %d\n", use_lf_delta); if (use_lf_delta) { int update; GET_BITS(update, 1); printf(" Update lf delta: %d\n", update); if (update) { int i; for (i = 0; i < 4 + 4; ++i) { int temp; GET_BITS(temp, 1); if (temp) GET_BITS(temp, 7); } } } return WEBP_INFO_OK; } static WebPInfoStatus ParseLossyHeader(const ChunkData* const chunk_data, const WebPInfo* const webp_info) { const uint8_t* data = chunk_data->payload_; size_t data_size = chunk_data->size_ - CHUNK_HEADER_SIZE; const uint32_t bits = (uint32_t)data[0] | (data[1] << 8) | (data[2] << 16); const int key_frame = !(bits & 1); const int profile = (bits >> 1) & 7; const int display = (bits >> 4) & 1; const uint32_t partition0_length = (bits >> 5); WebPInfoStatus status = WEBP_INFO_OK; uint64_t bit_position = 0; uint64_t* const bit_pos = &bit_position; int color_space, clamp_type; printf(" Parsing lossy bitstream...\n"); // Calling WebPGetFeatures() in ProcessImageChunk() should ensure this. assert(chunk_data->size_ >= CHUNK_HEADER_SIZE + 10); if (profile > 3) { LOG_ERROR("Unknown profile."); return WEBP_INFO_BITSTREAM_ERROR; } if (!display) { LOG_ERROR("Frame is not displayable."); return WEBP_INFO_BITSTREAM_ERROR; } data += 3; data_size -= 3; printf(" Key frame: %s\n" " Profile: %d\n" " Display: %s\n" " Part. 0 length: %d\n", key_frame ? "Yes" : "No", profile, display ? "Yes" : "No", partition0_length); if (key_frame) { if (!(data[0] == 0x9d && data[1] == 0x01 && data[2] == 0x2a)) { LOG_ERROR("Invalid lossy bitstream signature."); return WEBP_INFO_BITSTREAM_ERROR; } printf(" Width: %d\n" " X scale: %d\n" " Height: %d\n" " Y scale: %d\n", ((data[4] << 8) | data[3]) & 0x3fff, data[4] >> 6, ((data[6] << 8) | data[5]) & 0x3fff, data[6] >> 6); data += 7; data_size -= 7; } else { LOG_ERROR("Non-keyframe detected in lossy bitstream."); return WEBP_INFO_BITSTREAM_ERROR; } if (partition0_length >= data_size) { LOG_ERROR("Bad partition length."); return WEBP_INFO_BITSTREAM_ERROR; } GET_BITS(color_space, 1); GET_BITS(clamp_type, 1); printf(" Color space: %d\n", color_space); printf(" Clamp type: %d\n", clamp_type); status = ParseLossySegmentHeader(webp_info, data, data_size, bit_pos); if (status != WEBP_INFO_OK) return status; status = ParseLossyFilterHeader(webp_info, data, data_size, bit_pos); if (status != WEBP_INFO_OK) return status; { // Partition number and size. const uint8_t* part_size = data + partition0_length; int num_parts, i; size_t part_data_size; GET_BITS(num_parts, 2); num_parts = 1 << num_parts; if ((int)(data_size - partition0_length) < (num_parts - 1) * 3) { LOG_ERROR("Truncated lossy bitstream."); return WEBP_INFO_TRUNCATED_DATA; } part_data_size = data_size - partition0_length - (num_parts - 1) * 3; printf(" Total partitions: %d\n", num_parts); for (i = 1; i < num_parts; ++i) { const size_t psize = part_size[0] | (part_size[1] << 8) | (part_size[2] << 16); if (psize > part_data_size) { LOG_ERROR("Truncated partition."); return WEBP_INFO_TRUNCATED_DATA; } printf(" Part. %d length: %d\n", i, (int)psize); part_data_size -= psize; part_size += 3; } } // Quantizer. { int base_q, bit; int dq_y1_dc = 0, dq_y2_dc = 0, dq_y2_ac = 0, dq_uv_dc = 0, dq_uv_ac = 0; GET_BITS(base_q, 7); GET_BITS(bit, 1); if (bit) GET_SIGNED_BITS(dq_y1_dc, 4); GET_BITS(bit, 1); if (bit) GET_SIGNED_BITS(dq_y2_dc, 4); GET_BITS(bit, 1); if (bit) GET_SIGNED_BITS(dq_y2_ac, 4); GET_BITS(bit, 1); if (bit) GET_SIGNED_BITS(dq_uv_dc, 4); GET_BITS(bit, 1); if (bit) GET_SIGNED_BITS(dq_uv_ac, 4); printf(" Base Q: %d\n", base_q); printf(" DQ Y1 DC: %d\n", dq_y1_dc); printf(" DQ Y2 DC: %d\n", dq_y2_dc); printf(" DQ Y2 AC: %d\n", dq_y2_ac); printf(" DQ UV DC: %d\n", dq_uv_dc); printf(" DQ UV AC: %d\n", dq_uv_ac); } if ((*bit_pos >> 3) >= partition0_length) { LOG_ERROR("Truncated lossy bitstream."); return WEBP_INFO_TRUNCATED_DATA; } return WEBP_INFO_OK; } // ----------------------------------------------------------------------------- // Lossless bitstream analysis. static int LLGetBits(const uint8_t* const data, size_t data_size, size_t nb, int* val, uint64_t* const bit_pos) { uint32_t i = 0; *val = 0; while (i < nb) { const uint64_t p = (*bit_pos)++; if ((p >> 3) >= data_size) { return 0; } else { const int bit = !!(data[p >> 3] & (1 << ((p & 7)))); *val = *val | (bit << i); ++i; } } return 1; } #define LL_GET_BITS(v, n) \ do { \ if (!LLGetBits(data, data_size, n, &v, bit_pos)) { \ LOG_ERROR("Truncated lossless bitstream."); \ return WEBP_INFO_TRUNCATED_DATA; \ } \ } while (0) static WebPInfoStatus ParseLosslessTransform(WebPInfo* const webp_info, const uint8_t* const data, size_t data_size, uint64_t* const bit_pos) { int use_transform, block_size, n_colors; LL_GET_BITS(use_transform, 1); printf(" Use transform: %s\n", use_transform ? "Yes" : "No"); if (use_transform) { int type; LL_GET_BITS(type, 2); printf(" 1st transform: %s (%d)\n", kLosslessTransforms[type], type); switch (type) { case PREDICTOR_TRANSFORM: case CROSS_COLOR_TRANSFORM: LL_GET_BITS(block_size, 3); block_size = 1 << (block_size + 2); printf(" Tran. block size: %d\n", block_size); break; case COLOR_INDEXING_TRANSFORM: LL_GET_BITS(n_colors, 8); n_colors += 1; printf(" No. of colors: %d\n", n_colors); break; default: break; } } return WEBP_INFO_OK; } static WebPInfoStatus ParseLosslessHeader(const ChunkData* const chunk_data, WebPInfo* const webp_info) { const uint8_t* data = chunk_data->payload_; size_t data_size = chunk_data->size_ - CHUNK_HEADER_SIZE; uint64_t bit_position = 0; uint64_t* const bit_pos = &bit_position; WebPInfoStatus status; printf(" Parsing lossless bitstream...\n"); if (data_size < VP8L_FRAME_HEADER_SIZE) { LOG_ERROR("Truncated lossless bitstream."); return WEBP_INFO_TRUNCATED_DATA; } if (data[0] != VP8L_MAGIC_BYTE) { LOG_ERROR("Invalid lossless bitstream signature."); return WEBP_INFO_BITSTREAM_ERROR; } data += 1; data_size -= 1; { int width, height, has_alpha, version; LL_GET_BITS(width, 14); LL_GET_BITS(height, 14); LL_GET_BITS(has_alpha, 1); LL_GET_BITS(version, 3); width += 1; height += 1; printf(" Width: %d\n", width); printf(" Height: %d\n", height); printf(" Alpha: %d\n", has_alpha); printf(" Version: %d\n", version); } status = ParseLosslessTransform(webp_info, data, data_size, bit_pos); if (status != WEBP_INFO_OK) return status; return WEBP_INFO_OK; } static WebPInfoStatus ParseAlphaHeader(const ChunkData* const chunk_data, WebPInfo* const webp_info) { const uint8_t* data = chunk_data->payload_; size_t data_size = chunk_data->size_ - CHUNK_HEADER_SIZE; if (data_size <= ALPHA_HEADER_LEN) { LOG_ERROR("Truncated ALPH chunk."); return WEBP_INFO_TRUNCATED_DATA; } printf(" Parsing ALPH chunk...\n"); { const int compression_method = (data[0] >> 0) & 0x03; const int filter = (data[0] >> 2) & 0x03; const int pre_processing = (data[0] >> 4) & 0x03; const int reserved_bits = (data[0] >> 6) & 0x03; printf(" Compression: %d\n", compression_method); printf(" Filter: %s (%d)\n", kAlphaFilterMethods[filter], filter); printf(" Pre-processing: %d\n", pre_processing); if (compression_method > ALPHA_LOSSLESS_COMPRESSION) { LOG_ERROR("Invalid Alpha compression method."); return WEBP_INFO_BITSTREAM_ERROR; } if (pre_processing > ALPHA_PREPROCESSED_LEVELS) { LOG_ERROR("Invalid Alpha pre-processing method."); return WEBP_INFO_BITSTREAM_ERROR; } if (reserved_bits != 0) { LOG_WARN("Reserved bits in ALPH chunk header are not all 0."); } data += ALPHA_HEADER_LEN; data_size -= ALPHA_HEADER_LEN; if (compression_method == ALPHA_LOSSLESS_COMPRESSION) { uint64_t bit_pos = 0; WebPInfoStatus status = ParseLosslessTransform(webp_info, data, data_size, &bit_pos); if (status != WEBP_INFO_OK) return status; } } return WEBP_INFO_OK; } // ----------------------------------------------------------------------------- // Chunk parsing. 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(" 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:(ARGB) %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_) { assert(features.format >= 0 && features.format <= 2); printf(" Width: %d\n Height: %d\n Alpha: %d\n Animation: %d\n" " Format: %s (%d)\n", features.width, features.height, features.has_alpha, features.has_animation, kFormats[features.format], 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_WARN("Both VP8L and ALPH chunks are detected."); } 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; if (webp_info->parse_bitstream_) { const int is_lossy = (chunk_data->id_ == CHUNK_VP8); const WebPInfoStatus status = is_lossy ? ParseLossyHeader(chunk_data, webp_info) : ParseLosslessHeader(chunk_data, webp_info); if (status != WEBP_INFO_OK) return status; } return WEBP_INFO_OK; } static WebPInfoStatus ProcessALPHChunk(const ChunkData* const chunk_data, WebPInfo* const webp_info) { 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; if (webp_info->parse_bitstream_) { const WebPInfoStatus status = ParseAlphaHeader(chunk_data, webp_info); if (status != WEBP_INFO_OK) return status; } 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); const int alpha = !!(webp_info->feature_flags_ & ALPHA_FLAG); if (!alpha && webp_info->has_alpha_) { LOG_ERROR("Unexpected alpha data detected."); return WEBP_INFO_PARSE_ERROR; } if (alpha && !webp_info->has_alpha_) { LOG_WARN("Alpha flag is set with no alpha data present."); } 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" " -bitstream_info .... Parse bitstream header.\n"); } int main(int argc, const char* argv[]) { int c, quiet = 0, show_diag = 0, show_summary = 0; int parse_bitstream = 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 if (!strcmp(argv[c], "-bitstream_info")) { parse_bitstream = 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; webp_info.parse_bitstream_ = parse_bitstream; 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; }