diff --git a/examples/gif2webp.c b/examples/gif2webp.c index 9b224df4..6f1b5de3 100644 --- a/examples/gif2webp.c +++ b/examples/gif2webp.c @@ -36,11 +36,60 @@ static int transparent_index = -1; // No transparency by default. -static void ClearPicture(WebPPicture* const picture) { - int x, y; - for (y = 0; y < picture->height; ++y) { - uint32_t* const dst = picture->argb + y * picture->argb_stride; - for (x = 0; x < picture->width; ++x) dst[x] = TRANSPARENT_COLOR; +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; + } + } } } @@ -60,25 +109,104 @@ static void Remap(const uint8_t* const src, const GifFileType* const gif, } } -static int ReadSubImage(GifFileType* gif, WebPPicture* pic, WebPPicture* view) { +// 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; - const int offset_x = image_desc.Left; - const int offset_y = image_desc.Top; - const int sub_w = image_desc.Width; - const int sub_h = image_desc.Height; 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(pic, offset_x, offset_y, sub_w, sub_h, view)) { + 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", - sub_w, sub_h, offset_x, offset_y); + rect.width, rect.height, rect.x_offset, rect.y_offset); goto End; } - dst = view->argb; + dst = sub_image->argb; - tmp = (uint8_t*)malloc(sub_w * sizeof(*tmp)); + tmp = (uint8_t*)malloc(rect.width * sizeof(*tmp)); if (tmp == NULL) goto End; if (image_desc.Interlace) { // Interlaced image. @@ -88,21 +216,56 @@ static int ReadSubImage(GifFileType* gif, WebPPicture* pic, WebPPicture* view) { int pass; for (pass = 0; pass < 4; ++pass) { int y; - for (y = interlace_offsets[pass]; y < sub_h; y += interlace_jumps[pass]) { - if (DGifGetLine(gif, tmp, sub_w) == GIF_ERROR) goto End; - Remap(tmp, gif, dst + y * view->argb_stride, sub_w); + 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 < sub_h; ++y) { - if (DGifGetLine(gif, tmp, sub_w) == GIF_ERROR) goto End; - Remap(tmp, gif, dst + y * view->argb_stride, sub_w); + 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); } } - // re-align the view with even offset (and adjust dimensions if needed). - WebPPictureView(pic, offset_x & ~1, offset_y & ~1, - sub_w + (offset_x & 1), sub_h + (offset_y & 1), view); + + // 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: @@ -171,6 +334,7 @@ static void Help(void) { 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"); @@ -189,11 +353,14 @@ int main(int argc, const char *argv[]) { const char *in_file = NULL, *out_file = NULL; FILE* out = NULL; GifFileType* gif = NULL; - WebPPicture picture; + 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; @@ -206,8 +373,10 @@ int main(int argc, const char *argv[]) { 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(&picture)) { + if (!WebPConfigInit(&config) || + !WebPPictureInit(¤t) || !WebPPictureInit(&previous)) { fprintf(stderr, "Error! Version mismatch!\n"); return -1; } @@ -230,6 +399,9 @@ int main(int argc, const char *argv[]) { 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")) { @@ -272,11 +444,12 @@ int main(int argc, const char *argv[]) { #endif if (gif == NULL) goto End; - // Allocate picture buffer - picture.width = gif->SWidth; - picture.height = gif->SHeight; - picture.use_argb = 1; - if (!WebPPictureAlloc(&picture)) 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) { @@ -293,14 +466,15 @@ int main(int argc, const char *argv[]) { switch (type) { case IMAGE_DESC_RECORD_TYPE: { WebPPicture sub_image; + WebPFrameRect gif_rect; WebPMemoryWriter memory; - if (frame.dispose_method == WEBP_MUX_DISPOSE_BACKGROUND) { - ClearPicture(&picture); - } - if (!DGifGetImageDesc(gif)) goto End; - if (!ReadSubImage(gif, &picture, &sub_image)) 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 @@ -317,7 +491,7 @@ int main(int argc, const char *argv[]) { sub_image.custom_ptr = &memory; WebPMemoryWriterInit(&memory); if (!WebPEncode(&config, &sub_image)) { - fprintf(stderr, "Error! Cannot encode picture as WebP\n"); + fprintf(stderr, "Error! Cannot encode current as WebP\n"); fprintf(stderr, "Error code: %d\n", sub_image.error_code); goto End; } @@ -325,8 +499,6 @@ int main(int argc, const char *argv[]) { // 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.x_offset = gif->Image.Left & ~1; - frame.y_offset = gif->Image.Top & ~1; frame.bitstream.bytes = memory.mem; frame.bitstream.size = memory.size; err = WebPMuxPushFrame(mux, &frame, 1); @@ -340,11 +512,17 @@ int main(int argc, const char *argv[]) { sub_image.width, sub_image.height, frame.x_offset, frame.y_offset, frame.duration); - printf("dispose:%d transparent index:%d\n", - frame.dispose_method, transparent_index); + 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: { @@ -380,8 +558,8 @@ int main(int argc, const char *argv[]) { fprintf(stderr, "GIF decode warning: invalid background color " "index. Assuming white background.\n"); } - ClearPicture(&picture); - is_first_frame = 0; + ClearPicture(¤t, NULL); + ClearPicture(&previous, NULL); } break; } @@ -520,7 +698,8 @@ int main(int argc, const char *argv[]) { End: WebPDataClear(&webp_data); WebPMuxDelete(mux); - WebPPictureFree(&picture); + WebPPictureFree(¤t); + WebPPictureFree(&previous); if (out != NULL && out_file != NULL) fclose(out); if (gif_error != GIF_OK) { diff --git a/man/gif2webp.1 b/man/gif2webp.1 index 2b88bb9a..f2e0fd1a 100644 --- a/man/gif2webp.1 +++ b/man/gif2webp.1 @@ -49,6 +49,19 @@ additional encoding possibilities and decide on the quality gain. Lower value can result is faster processing time at the expense of larger file size and lower compression quality. .TP +.BI \-kmax " int +Relevant only for animated images with large number of frames (>50). +Specify the maximum distance between consecutive key frames (independently +decodable frames) in the output image. The tool will insert some key frames into +the output image as needed so that this criteria is satisfied. A value of 0 will +turn off insertion of key frames. +Typical values are in the range 5 to 30. Default value is 9. +When lower values are used, more frames will be converted to key frames. This +may lead to smaller number of frames required to decode a frame on average, +thereby improving the decoding performance. But this may lead to slightly bigger +file sizes. +Higher values may lead to worse decoding performance, but smaller file sizes. +.TP .BI \-f " int For lossy encoding only (specified by the \-lossy option). Specify the strength of the deblocking filter, between 0 (no filtering) and 100 (maximum filtering).