// Copyright 2012 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. // ----------------------------------------------------------------------------- // // simple tool to convert animated GIFs to WebP // // Authors: Skal (pascal.massimino@gmail.com) // Urvang (urvang@google.com) #include #include #include #include #ifdef HAVE_CONFIG_H #include "config.h" #endif #include #include "webp/encode.h" #include "webp/mux.h" #include "./example_util.h" #define GIF_TRANSPARENT_MASK 0x01 #define GIF_DISPOSE_MASK 0x07 #define GIF_DISPOSE_SHIFT 2 #define TRANSPARENT_COLOR 0x00ffffff #define WHITE_COLOR 0xffffffff //------------------------------------------------------------------------------ static int transparent_index = -1; // No transparency by default. static void ClearRectangle(WebPPicture* const picture, int left, int top, int width, int height) { int j; for (j = top; j < top + height; ++j) { uint32_t* const dst = picture->argb + j * picture->argb_stride; int i; for (i = left; i < left + width; ++i) { dst[i] = TRANSPARENT_COLOR; } } } typedef struct WebPFrameRect { int x_offset, y_offset, width, height; } WebPFrameRect; // Clear pixels in 'picture' within given 'rect' to transparent color. static void ClearPicture(WebPPicture* const picture, const WebPFrameRect* const rect) { if (rect != NULL) { ClearRectangle(picture, rect->x_offset, rect->y_offset, rect->width, rect->height); } else { ClearRectangle(picture, 0, 0, picture->width, picture->height); } } // TODO: Also used in picture.c. Move to a common location. static void CopyPlane(const uint8_t* src, int src_stride, uint8_t* dst, int dst_stride, int width, int height) { while (height-- > 0) { memcpy(dst, src, width); src += src_stride; dst += dst_stride; } } // Given 'curr_frame' and its 'rect', blend it in the 'canvas'. static void BlendCurrentFrameInCanvas(const WebPPicture* const curr_frame, const WebPFrameRect* const rect, WebPPicture* const canvas) { int j; assert(canvas->width == curr_frame->width && canvas->height == curr_frame->height); for (j = rect->y_offset; j < rect->y_offset + rect->height; ++j) { int i; for (i = rect->x_offset; i < rect->x_offset + rect->width; ++i) { const uint32_t src = curr_frame->argb[j * curr_frame->argb_stride + i]; const int src_alpha = src >> 24; if (src_alpha != 0) { canvas->argb[j * canvas->argb_stride + i] = src; } } } } static void Remap(const uint8_t* const src, const GifFileType* const gif, uint32_t* dst, int len) { int i; const GifColorType* colors; const ColorMapObject* const cmap = gif->Image.ColorMap ? gif->Image.ColorMap : gif->SColorMap; if (cmap == NULL) return; colors = cmap->Colors; for (i = 0; i < len; ++i) { const GifColorType c = colors[src[i]]; dst[i] = (src[i] == transparent_index) ? TRANSPARENT_COLOR : c.Blue | (c.Green << 8) | (c.Red << 16) | (0xff << 24); } } // Returns true if 'curr' frame is a key frame, that is, it can be decoded // independently of 'prev' canvas. static int IsKeyFrame(const WebPPicture* const curr, const WebPFrameRect* const curr_rect, const WebPPicture* const prev) { int i, j; int is_key_frame = 1; // If previous canvas (with previous frame disposed) is all transparent, // current frame is a key frame. for (i = 0; i < prev->width; ++i) { for (j = 0; j < prev->height; ++j) { const uint32_t prev_alpha = (prev->argb[j * prev->argb_stride + i]) >> 24; if (prev_alpha != 0) { is_key_frame = 0; break; } } if (!is_key_frame) break; } if (is_key_frame) return 1; // If current frame covers the whole canvas and does not contain any // transparent pixels that depend on previous canvas, then current frame is // a key frame. if (curr_rect->width == curr->width && curr_rect->height == curr->height) { assert(curr_rect->x_offset == 0 && curr_rect->y_offset == 0); is_key_frame = 1; for (j = 0; j < prev->height; ++j) { for (i = 0; i < prev->width; ++i) { const uint32_t prev_alpha = (prev->argb[j * prev->argb_stride + i]) >> 24; const uint32_t curr_alpha = (curr->argb[j * curr->argb_stride + i]) >> 24; if (curr_alpha != 0xff && prev_alpha != 0) { is_key_frame = 0; break; } } if (!is_key_frame) break; } if (is_key_frame) return 1; } return 0; } // Convert 'curr' frame to a key frame. static void ConvertToKeyFrame(WebPPicture* const curr, const WebPPicture* const prev, WebPFrameRect* const rect) { int j; assert(curr->width == prev->width && curr->height == prev->height); // Replace transparent pixels of current canvas with those from previous // canvas (with previous frame disposed). for (j = 0; j < curr->height; ++j) { int i; for (i = 0; i < curr->width; ++i) { uint32_t* const curr_pixel = curr->argb + j * curr->argb_stride + i; const int curr_alpha = *curr_pixel >> 24; if (curr_alpha == 0) { *curr_pixel = prev->argb[j * prev->argb_stride + i]; } } } // Frame rectangle now covers the whole canvas. rect->x_offset = 0; rect->y_offset = 0; rect->width = curr->width; rect->height = curr->height; } static int ReadSubImage(GifFileType* gif, WebPPicture* const prev, int is_first_frame, size_t key_frame_interval, size_t* const count_since_key_frame, WebPFrameRect* const gif_rect, WebPPicture* sub_image, WebPMuxFrameInfo* const info, WebPPicture* const curr) { const GifImageDesc image_desc = gif->Image; uint32_t* dst = NULL; uint8_t* tmp = NULL; int ok = 0; WebPFrameRect rect = { image_desc.Left, image_desc.Top, image_desc.Width, image_desc.Height }; *gif_rect = rect; // Use a view for the sub-picture: if (!WebPPictureView(curr, rect.x_offset, rect.y_offset, rect.width, rect.height, sub_image)) { fprintf(stderr, "Sub-image %dx%d at position %d,%d is invalid!\n", rect.width, rect.height, rect.x_offset, rect.y_offset); goto End; } dst = sub_image->argb; tmp = (uint8_t*)malloc(rect.width * sizeof(*tmp)); if (tmp == NULL) goto End; if (image_desc.Interlace) { // Interlaced image. // We need 4 passes, with the following offsets and jumps. const int interlace_offsets[] = { 0, 4, 2, 1 }; const int interlace_jumps[] = { 8, 8, 4, 2 }; int pass; for (pass = 0; pass < 4; ++pass) { int y; for (y = interlace_offsets[pass]; y < rect.height; y += interlace_jumps[pass]) { if (DGifGetLine(gif, tmp, rect.width) == GIF_ERROR) goto End; Remap(tmp, gif, dst + y * sub_image->argb_stride, rect.width); } } } else { // Non-interlaced image. int y; for (y = 0; y < rect.height; ++y) { if (DGifGetLine(gif, tmp, rect.width) == GIF_ERROR) goto End; Remap(tmp, gif, dst + y * sub_image->argb_stride, rect.width); } } // Snap to even offsets (and adjust dimensions if needed). rect.width += (rect.x_offset & 1); rect.height += (rect.y_offset & 1); rect.x_offset &= ~1; rect.y_offset &= ~1; // Make this a key frame if needed. if (is_first_frame || IsKeyFrame(curr, &rect, prev)) { *count_since_key_frame = 0; } else { ++*count_since_key_frame; if (*count_since_key_frame > key_frame_interval) { ConvertToKeyFrame(curr, prev, &rect); *count_since_key_frame = 0; } } if (*count_since_key_frame == 0) { info->blend_method = WEBP_MUX_NO_BLEND; // Key frame, so no need to blend. } else { info->blend_method = WEBP_MUX_BLEND; // The blending method in GIF. } // Update the canvas so that it can be used to check for and/or create an // key frame in the next iterations. if (info->blend_method == WEBP_MUX_NO_BLEND) { CopyPlane((uint8_t*)curr->argb, 4 * curr->argb_stride, (uint8_t*)prev->argb, 4 * prev->argb_stride, 4 * curr->width, curr->height); } else { BlendCurrentFrameInCanvas(curr, gif_rect, prev); } WebPPictureView(curr, rect.x_offset, rect.y_offset, rect.width, rect.height, sub_image); info->x_offset = rect.x_offset; info->y_offset = rect.y_offset; ok = 1; End: free(tmp); return ok; } static int GetBackgroundColor(const ColorMapObject* const color_map, GifWord bgcolor_idx, uint32_t* const bgcolor) { if (transparent_index != -1 && bgcolor_idx == transparent_index) { *bgcolor = TRANSPARENT_COLOR; // Special case. return 1; } else if (color_map == NULL || color_map->Colors == NULL || bgcolor_idx >= color_map->ColorCount) { return 0; // Invalid color map or index. } else { const GifColorType color = color_map->Colors[bgcolor_idx]; *bgcolor = (0xff << 24) | (color.Red << 16) | (color.Green << 8) | (color.Blue << 0); return 1; } } static void DisplayGifError(const GifFileType* const gif, int gif_error) { // GIFLIB_MAJOR is only defined in libgif >= 4.2.0. // libgif 4.2.0 has retired PrintGifError() and added GifErrorString(). #if defined(GIFLIB_MAJOR) && defined(GIFLIB_MINOR) && \ ((GIFLIB_MAJOR == 4 && GIFLIB_MINOR >= 2) || GIFLIB_MAJOR > 4) #if GIFLIB_MAJOR >= 5 // Static string actually, hence the const char* cast. const char* error_str = (const char*)GifErrorString( (gif == NULL) ? gif_error : gif->Error); #else const char* error_str = (const char*)GifErrorString(); (void)gif; #endif if (error_str == NULL) error_str = "Unknown error"; fprintf(stderr, "GIFLib Error %d: %s\n", gif_error, error_str); #else (void)gif; fprintf(stderr, "GIFLib Error %d: ", gif_error); PrintGifError(); fprintf(stderr, "\n"); #endif } static const char* const kErrorMessages[] = { "WEBP_MUX_NOT_FOUND", "WEBP_MUX_INVALID_ARGUMENT", "WEBP_MUX_BAD_DATA", "WEBP_MUX_MEMORY_ERROR", "WEBP_MUX_NOT_ENOUGH_DATA" }; static const char* ErrorString(WebPMuxError err) { assert(err <= WEBP_MUX_NOT_FOUND && err >= WEBP_MUX_NOT_ENOUGH_DATA); return kErrorMessages[-err]; } //------------------------------------------------------------------------------ static void Help(void) { printf("Usage:\n"); printf(" gif2webp [options] gif_file -o webp_file\n"); printf("options:\n"); printf(" -h / -help ............ this help\n"); printf(" -lossy ................. Encode image using lossy compression.\n"); printf(" -q ............. quality factor (0:small..100:big)\n"); printf(" -m ............... compression method (0=fast, 6=slowest)\n"); printf(" -kmax ............ Max distance between key frames\n"); printf(" -f ............... filter strength (0=off..100)\n"); printf("\n"); printf(" -version ............... print version number and exit.\n"); printf(" -v ..................... verbose.\n"); printf(" -quiet ................. don't print anything.\n"); printf("\n"); } //------------------------------------------------------------------------------ int main(int argc, const char *argv[]) { int verbose = 0; int gif_error = GIF_ERROR; WebPMuxError err = WEBP_MUX_OK; int ok = 0; const char *in_file = NULL, *out_file = NULL; FILE* out = NULL; GifFileType* gif = NULL; WebPPicture current; WebPPicture previous; WebPMuxFrameInfo frame; WebPMuxAnimParams anim = { WHITE_COLOR, 0 }; int is_first_frame = 1; size_t count_since_key_frame = 0; // Frames seen since the last key frame. size_t key_frame_interval = 9; // Max distance between key frames. int done; int c; int quiet = 0; WebPConfig config; WebPMux* mux = NULL; WebPData webp_data = { NULL, 0 }; int stored_icc = 0; // Whether we have already stored an ICC profile. int stored_xmp = 0; memset(&frame, 0, sizeof(frame)); frame.id = WEBP_CHUNK_ANMF; frame.dispose_method = WEBP_MUX_DISPOSE_BACKGROUND; frame.blend_method = WEBP_MUX_BLEND; if (!WebPConfigInit(&config) || !WebPPictureInit(¤t) || !WebPPictureInit(&previous)) { fprintf(stderr, "Error! Version mismatch!\n"); return -1; } config.lossless = 1; // Use lossless compression by default. if (argc == 1) { Help(); return 0; } for (c = 1; c < argc; ++c) { if (!strcmp(argv[c], "-h") || !strcmp(argv[c], "-help")) { Help(); return 0; } else if (!strcmp(argv[c], "-o") && c < argc - 1) { out_file = argv[++c]; } else if (!strcmp(argv[c], "-lossy")) { config.lossless = 0; } else if (!strcmp(argv[c], "-q") && c < argc - 1) { config.quality = (float)strtod(argv[++c], NULL); } else if (!strcmp(argv[c], "-m") && c < argc - 1) { config.method = strtol(argv[++c], NULL, 0); } else if (!strcmp(argv[c], "-kmax") && c < argc - 1) { key_frame_interval = strtoul(argv[++c], NULL, 0); if (key_frame_interval == 0) key_frame_interval = ~0; } else if (!strcmp(argv[c], "-f") && c < argc - 1) { config.filter_strength = strtol(argv[++c], NULL, 0); } else if (!strcmp(argv[c], "-version")) { const int enc_version = WebPGetEncoderVersion(); const int mux_version = WebPGetMuxVersion(); printf("WebP Encoder version: %d.%d.%d\nWebP Mux version: %d.%d.%d\n", (enc_version >> 16) & 0xff, (enc_version >> 8) & 0xff, enc_version & 0xff, (mux_version >> 16) & 0xff, (mux_version >> 8) & 0xff, mux_version & 0xff); return 0; } else if (!strcmp(argv[c], "-quiet")) { quiet = 1; } else if (!strcmp(argv[c], "-v")) { verbose = 1; } else if (argv[c][0] == '-') { fprintf(stderr, "Error! Unknown option '%s'\n", argv[c]); Help(); return -1; } else { in_file = argv[c]; } } if (!WebPValidateConfig(&config)) { fprintf(stderr, "Error! Invalid configuration.\n"); goto End; } if (in_file == NULL) { fprintf(stderr, "No input file specified!\n"); Help(); goto End; } // Start the decoder object #if defined(GIFLIB_MAJOR) && (GIFLIB_MAJOR >= 5) // There was an API change in version 5.0.0. gif = DGifOpenFileName(in_file, &gif_error); #else gif = DGifOpenFileName(in_file); #endif if (gif == NULL) goto End; // Allocate current buffer current.width = gif->SWidth; current.height = gif->SHeight; current.use_argb = 1; if (!WebPPictureAlloc(¤t)) goto End; if (!WebPPictureCopy(¤t, &previous)) goto End; mux = WebPMuxNew(); if (mux == NULL) { fprintf(stderr, "ERROR: could not create a mux object.\n"); goto End; } // Loop over GIF images done = 0; do { GifRecordType type; if (DGifGetRecordType(gif, &type) == GIF_ERROR) goto End; switch (type) { case IMAGE_DESC_RECORD_TYPE: { WebPPicture sub_image; WebPFrameRect gif_rect; WebPMemoryWriter memory; if (!DGifGetImageDesc(gif)) goto End; if (!ReadSubImage(gif, &previous, is_first_frame, key_frame_interval, &count_since_key_frame, &gif_rect, &sub_image, &frame, ¤t)) { goto End; } if (!config.lossless) { // We need to call BGRA variant because of the way we do Remap(). Note // that 'sub_image' will no longer be a view and own some memory. WebPPictureImportBGRA( &sub_image, (uint8_t*)sub_image.argb, sub_image.argb_stride * sizeof(*sub_image.argb)); sub_image.use_argb = 0; } else { sub_image.use_argb = 1; } sub_image.writer = WebPMemoryWrite; sub_image.custom_ptr = &memory; WebPMemoryWriterInit(&memory); if (!WebPEncode(&config, &sub_image)) { fprintf(stderr, "Error! Cannot encode current as WebP\n"); fprintf(stderr, "Error code: %d\n", sub_image.error_code); goto End; } // Now we have all the info about the frame, as a Graphic Control // Extension Block always appears before the Image Descriptor Block. // So add the frame to mux. frame.bitstream.bytes = memory.mem; frame.bitstream.size = memory.size; err = WebPMuxPushFrame(mux, &frame, 1); if (err != WEBP_MUX_OK) { fprintf(stderr, "ERROR (%s): Could not add animation frame.\n", ErrorString(err)); goto End; } if (verbose) { printf("Added frame %dx%d (offset:%d,%d duration:%d) ", sub_image.width, sub_image.height, frame.x_offset, frame.y_offset, frame.duration); printf("dispose:%d blend:%d transparent index:%d\n", frame.dispose_method, frame.blend_method, transparent_index); } WebPDataClear(&frame.bitstream); WebPPictureFree(&sub_image); is_first_frame = 0; if (frame.dispose_method == WEBP_MUX_DISPOSE_BACKGROUND) { ClearPicture(¤t, NULL); ClearPicture(&previous, &gif_rect); } break; } case EXTENSION_RECORD_TYPE: { int extension; GifByteType *data = NULL; if (DGifGetExtension(gif, &extension, &data) == GIF_ERROR) { goto End; } switch (extension) { case COMMENT_EXT_FUNC_CODE: { break; // Do nothing for now. } case GRAPHICS_EXT_FUNC_CODE: { const int flags = data[1]; const int dispose = (flags >> GIF_DISPOSE_SHIFT) & GIF_DISPOSE_MASK; const int delay = data[2] | (data[3] << 8); // In 10 ms units. if (data[0] != 4) goto End; frame.duration = delay * 10; // Duration is in 1 ms units for WebP. if (dispose == 3) { fprintf(stderr, "WARNING: GIF_DISPOSE_RESTORE not supported."); // failsafe. TODO(urvang): emulate the correct behaviour by // recoding the whole frame. frame.dispose_method = WEBP_MUX_DISPOSE_BACKGROUND; } else { frame.dispose_method = (dispose == 2) ? WEBP_MUX_DISPOSE_BACKGROUND : WEBP_MUX_DISPOSE_NONE; } transparent_index = (flags & GIF_TRANSPARENT_MASK) ? data[4] : -1; if (is_first_frame) { if (!GetBackgroundColor(gif->SColorMap, gif->SBackGroundColor, &anim.bgcolor)) { fprintf(stderr, "GIF decode warning: invalid background color " "index. Assuming white background.\n"); } ClearPicture(¤t, NULL); ClearPicture(&previous, NULL); } break; } case PLAINTEXT_EXT_FUNC_CODE: { break; } case APPLICATION_EXT_FUNC_CODE: { if (data[0] != 11) break; // Chunk is too short if (!memcmp(data + 1, "NETSCAPE2.0", 11)) { // Recognize and parse Netscape2.0 NAB extension for loop count. if (DGifGetExtensionNext(gif, &data) == GIF_ERROR) goto End; if (data == NULL) goto End; // Loop count sub-block missing. if (data[0] != 3 && data[1] != 1) break; // wrong size/marker anim.loop_count = data[2] | (data[3] << 8); if (verbose) printf("Loop count: %d\n", anim.loop_count); } else { // An extension containing metadata. // We only store the first encountered chunk of each type. const int is_xmp = !stored_xmp && !memcmp(data + 1, "XMP DataXMP", 11); const int is_icc = !stored_icc && !memcmp(data + 1, "ICCRGBG1012", 11); if (is_xmp || is_icc) { const char* const fourccs[2] = { "XMP " , "ICCP" }; const char* const features[2] = { "XMP" , "ICC" }; WebPData metadata = { NULL, 0 }; // Construct metadata from sub-blocks. // Usual case (including ICC profile): In each sub-block, the // first byte specifies its size in bytes (0 to 255) and the // rest of the bytes contain the data. // Special case for XMP data: In each sub-block, the first byte // is also part of the XMP payload. XMP in GIF also has a 257 // byte padding data. See the XMP specification for details. while (1) { WebPData prev_metadata = metadata; WebPData subblock; if (DGifGetExtensionNext(gif, &data) == GIF_ERROR) { WebPDataClear(&metadata); goto End; } if (data == NULL) break; // Finished. subblock.size = is_xmp ? data[0] + 1 : data[0]; assert(subblock.size > 0); subblock.bytes = is_xmp ? data : data + 1; metadata.bytes = (uint8_t*)realloc((void*)metadata.bytes, prev_metadata.size + subblock.size); if (metadata.bytes == NULL) { WebPDataClear(&prev_metadata); goto End; } metadata.size += subblock.size; memcpy((void*)(metadata.bytes + prev_metadata.size), subblock.bytes, subblock.size); } if (is_xmp) { // XMP padding data is 0x01, 0xff, 0xfe ... 0x01, 0x00. const size_t xmp_pading_size = 257; if (metadata.size > xmp_pading_size) { metadata.size -= xmp_pading_size; } } // Add metadata chunk. err = WebPMuxSetChunk(mux, fourccs[is_icc], &metadata, 1); if (verbose) { printf("%s size: %d\n", features[is_icc], (int)metadata.size); } WebPDataClear(&metadata); if (err != WEBP_MUX_OK) { fprintf(stderr, "ERROR (%s): Could not set %s chunk.\n", ErrorString(err), features[is_icc]); goto End; } if (is_icc) { stored_icc = 1; } else if (is_xmp) { stored_xmp = 1; } } } break; } default: { break; // skip } } while (data != NULL) { if (DGifGetExtensionNext(gif, &data) == GIF_ERROR) goto End; } break; } case TERMINATE_RECORD_TYPE: { done = 1; break; } default: { if (verbose) { fprintf(stderr, "Skipping over unknown record type %d\n", type); } break; } } } while (!done); // Finish muxing err = WebPMuxSetAnimationParams(mux, &anim); if (err != WEBP_MUX_OK) { fprintf(stderr, "ERROR (%s): Could not set animation parameters.\n", ErrorString(err)); goto End; } err = WebPMuxAssemble(mux, &webp_data); if (err != WEBP_MUX_OK) { fprintf(stderr, "ERROR (%s) assembling the WebP file.\n", ErrorString(err)); goto End; } if (out_file != NULL) { if (!ExUtilWriteFile(out_file, webp_data.bytes, webp_data.size)) { fprintf(stderr, "Error writing output file: %s\n", out_file); goto End; } if (!quiet) { printf("Saved output file: %s\n", out_file); } } else { if (!quiet) { printf("Nothing written; use -o flag to save the result.\n"); } } // All OK. ok = 1; gif_error = GIF_OK; End: WebPDataClear(&webp_data); WebPMuxDelete(mux); WebPPictureFree(¤t); WebPPictureFree(&previous); if (out != NULL && out_file != NULL) fclose(out); if (gif_error != GIF_OK) { DisplayGifError(gif, gif_error); } if (gif != NULL) { DGifCloseFile(gif); } return !ok; } //------------------------------------------------------------------------------