// Copyright 2015 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. // ----------------------------------------------------------------------------- // // Utilities for animated images #include "./anim_util.h" #include #include #include #include #if defined(WEBP_HAVE_GIF) #include #endif #include "webp/format_constants.h" #include "webp/decode.h" #include "webp/demux.h" #include "../imageio/imageio_util.h" #if defined(_MSC_VER) && _MSC_VER < 1900 #define snprintf _snprintf #endif static const int kNumChannels = 4; // ----------------------------------------------------------------------------- // Common utilities. #if defined(WEBP_HAVE_GIF) // Returns true if the frame covers the full canvas. static int IsFullFrame(int width, int height, int canvas_width, int canvas_height) { return (width == canvas_width && height == canvas_height); } #endif // WEBP_HAVE_GIF static int CheckSizeForOverflow(uint64_t size) { return (size == (size_t)size); } static int AllocateFrames(AnimatedImage* const image, uint32_t num_frames) { uint32_t i; uint8_t* mem = NULL; DecodedFrame* frames = NULL; const uint64_t rgba_size = (uint64_t)image->canvas_width * kNumChannels * image->canvas_height; const uint64_t total_size = (uint64_t)num_frames * rgba_size * sizeof(*mem); const uint64_t total_frame_size = (uint64_t)num_frames * sizeof(*frames); if (!CheckSizeForOverflow(total_size) || !CheckSizeForOverflow(total_frame_size)) { return 0; } mem = (uint8_t*)malloc((size_t)total_size); frames = (DecodedFrame*)malloc((size_t)total_frame_size); if (mem == NULL || frames == NULL) { free(mem); free(frames); return 0; } free(image->raw_mem); image->num_frames = num_frames; image->frames = frames; for (i = 0; i < num_frames; ++i) { frames[i].rgba = mem + i * rgba_size; frames[i].duration = 0; frames[i].is_key_frame = 0; } image->raw_mem = mem; return 1; } void ClearAnimatedImage(AnimatedImage* const image) { if (image != NULL) { free(image->raw_mem); free(image->frames); image->num_frames = 0; image->frames = NULL; image->raw_mem = NULL; } } #if defined(WEBP_HAVE_GIF) // Clear the canvas to transparent. static void ZeroFillCanvas(uint8_t* rgba, uint32_t canvas_width, uint32_t canvas_height) { memset(rgba, 0, canvas_width * kNumChannels * canvas_height); } // Clear given frame rectangle to transparent. static void ZeroFillFrameRect(uint8_t* rgba, int rgba_stride, int x_offset, int y_offset, int width, int height) { int j; assert(width * kNumChannels <= rgba_stride); rgba += y_offset * rgba_stride + x_offset * kNumChannels; for (j = 0; j < height; ++j) { memset(rgba, 0, width * kNumChannels); rgba += rgba_stride; } } // Copy width * height pixels from 'src' to 'dst'. static void CopyCanvas(const uint8_t* src, uint8_t* dst, uint32_t width, uint32_t height) { assert(src != NULL && dst != NULL); memcpy(dst, src, width * kNumChannels * height); } // Copy pixels in the given rectangle from 'src' to 'dst' honoring the 'stride'. static void CopyFrameRectangle(const uint8_t* src, uint8_t* dst, int stride, int x_offset, int y_offset, int width, int height) { int j; const int width_in_bytes = width * kNumChannels; const size_t offset = y_offset * stride + x_offset * kNumChannels; assert(width_in_bytes <= stride); src += offset; dst += offset; for (j = 0; j < height; ++j) { memcpy(dst, src, width_in_bytes); src += stride; dst += stride; } } #endif // WEBP_HAVE_GIF // Canonicalize all transparent pixels to transparent black to aid comparison. static void CleanupTransparentPixels(uint32_t* rgba, uint32_t width, uint32_t height) { const uint32_t* const rgba_end = rgba + width * height; while (rgba < rgba_end) { const uint8_t alpha = (*rgba >> 24) & 0xff; if (alpha == 0) { *rgba = 0; } ++rgba; } } // Dump frame to a PAM file. Returns true on success. static int DumpFrame(const char filename[], const char dump_folder[], uint32_t frame_num, const uint8_t rgba[], int canvas_width, int canvas_height) { int ok = 0; size_t max_len; int y; const char* base_name = NULL; char* file_name = NULL; FILE* f = NULL; const char* row; if (dump_folder == NULL) dump_folder = "."; base_name = strrchr(filename, '/'); base_name = (base_name == NULL) ? filename : base_name + 1; max_len = strlen(dump_folder) + 1 + strlen(base_name) + strlen("_frame_") + strlen(".pam") + 8; file_name = (char*)malloc(max_len * sizeof(*file_name)); if (file_name == NULL) goto End; if (snprintf(file_name, max_len, "%s/%s_frame_%d.pam", dump_folder, base_name, frame_num) < 0) { fprintf(stderr, "Error while generating file name\n"); goto End; } f = fopen(file_name, "wb"); if (f == NULL) { fprintf(stderr, "Error opening file for writing: %s\n", file_name); ok = 0; goto End; } if (fprintf(f, "P7\nWIDTH %d\nHEIGHT %d\n" "DEPTH 4\nMAXVAL 255\nTUPLTYPE RGB_ALPHA\nENDHDR\n", canvas_width, canvas_height) < 0) { fprintf(stderr, "Write error for file %s\n", file_name); goto End; } row = (const char*)rgba; for (y = 0; y < canvas_height; ++y) { if (fwrite(row, canvas_width * kNumChannels, 1, f) != 1) { fprintf(stderr, "Error writing to file: %s\n", file_name); goto End; } row += canvas_width * kNumChannels; } ok = 1; End: if (f != NULL) fclose(f); free(file_name); return ok; } // ----------------------------------------------------------------------------- // WebP Decoding. // Returns true if this is a valid WebP bitstream. static int IsWebP(const WebPData* const webp_data) { return (WebPGetInfo(webp_data->bytes, webp_data->size, NULL, NULL) != 0); } // Read animated WebP bitstream 'webp_data' into 'AnimatedImage' struct. static int ReadAnimatedWebP(const char filename[], const WebPData* const webp_data, AnimatedImage* const image, int dump_frames, const char dump_folder[]) { int ok = 0; int dump_ok = 1; uint32_t frame_index = 0; int prev_frame_timestamp = 0; WebPAnimDecoder* dec; WebPAnimInfo anim_info; memset(image, 0, sizeof(*image)); dec = WebPAnimDecoderNew(webp_data, NULL); if (dec == NULL) { fprintf(stderr, "Error parsing image: %s\n", filename); goto End; } if (!WebPAnimDecoderGetInfo(dec, &anim_info)) { fprintf(stderr, "Error getting global info about the animation\n"); goto End; } // Animation properties. image->canvas_width = anim_info.canvas_width; image->canvas_height = anim_info.canvas_height; image->loop_count = anim_info.loop_count; image->bgcolor = anim_info.bgcolor; // Allocate frames. if (!AllocateFrames(image, anim_info.frame_count)) return 0; // Decode frames. while (WebPAnimDecoderHasMoreFrames(dec)) { DecodedFrame* curr_frame; uint8_t* curr_rgba; uint8_t* frame_rgba; int timestamp; if (!WebPAnimDecoderGetNext(dec, &frame_rgba, ×tamp)) { fprintf(stderr, "Error decoding frame #%u\n", frame_index); goto End; } assert(frame_index < anim_info.frame_count); curr_frame = &image->frames[frame_index]; curr_rgba = curr_frame->rgba; curr_frame->duration = timestamp - prev_frame_timestamp; curr_frame->is_key_frame = 0; // Unused. memcpy(curr_rgba, frame_rgba, image->canvas_width * kNumChannels * image->canvas_height); // Needed only because we may want to compare with GIF later. CleanupTransparentPixels((uint32_t*)curr_rgba, image->canvas_width, image->canvas_height); if (dump_frames && dump_ok) { dump_ok = DumpFrame(filename, dump_folder, frame_index, curr_rgba, image->canvas_width, image->canvas_height); if (!dump_ok) { // Print error once, but continue decode loop. fprintf(stderr, "Error dumping frames to %s\n", dump_folder); } } ++frame_index; prev_frame_timestamp = timestamp; } ok = dump_ok; End: WebPAnimDecoderDelete(dec); return ok; } // ----------------------------------------------------------------------------- // GIF Decoding. #if defined(WEBP_HAVE_GIF) // Returns true if this is a valid GIF bitstream. static int IsGIF(const WebPData* const data) { return data->size > GIF_STAMP_LEN && (!memcmp(GIF_STAMP, data->bytes, GIF_STAMP_LEN) || !memcmp(GIF87_STAMP, data->bytes, GIF_STAMP_LEN) || !memcmp(GIF89_STAMP, data->bytes, GIF_STAMP_LEN)); } // GIFLIB_MAJOR is only defined in libgif >= 4.2.0. #if defined(GIFLIB_MAJOR) && defined(GIFLIB_MINOR) # define LOCAL_GIF_VERSION ((GIFLIB_MAJOR << 8) | GIFLIB_MINOR) # define LOCAL_GIF_PREREQ(maj, min) \ (LOCAL_GIF_VERSION >= (((maj) << 8) | (min))) #else # define LOCAL_GIF_VERSION 0 # define LOCAL_GIF_PREREQ(maj, min) 0 #endif #if !LOCAL_GIF_PREREQ(5, 0) // Added in v5.0 typedef struct { int DisposalMode; #define DISPOSAL_UNSPECIFIED 0 // No disposal specified #define DISPOSE_DO_NOT 1 // Leave image in place #define DISPOSE_BACKGROUND 2 // Set area to background color #define DISPOSE_PREVIOUS 3 // Restore to previous content int UserInputFlag; // User confirmation required before disposal int DelayTime; // Pre-display delay in 0.01sec units int TransparentColor; // Palette index for transparency, -1 if none #define NO_TRANSPARENT_COLOR -1 } GraphicsControlBlock; static int DGifExtensionToGCB(const size_t GifExtensionLength, const GifByteType* GifExtension, GraphicsControlBlock* gcb) { if (GifExtensionLength != 4) { return GIF_ERROR; } gcb->DisposalMode = (GifExtension[0] >> 2) & 0x07; gcb->UserInputFlag = (GifExtension[0] & 0x02) != 0; gcb->DelayTime = GifExtension[1] | (GifExtension[2] << 8); if (GifExtension[0] & 0x01) { gcb->TransparentColor = (int)GifExtension[3]; } else { gcb->TransparentColor = NO_TRANSPARENT_COLOR; } return GIF_OK; } static int DGifSavedExtensionToGCB(GifFileType* GifFile, int ImageIndex, GraphicsControlBlock* gcb) { int i; if (ImageIndex < 0 || ImageIndex > GifFile->ImageCount - 1) { return GIF_ERROR; } gcb->DisposalMode = DISPOSAL_UNSPECIFIED; gcb->UserInputFlag = 0; gcb->DelayTime = 0; gcb->TransparentColor = NO_TRANSPARENT_COLOR; for (i = 0; i < GifFile->SavedImages[ImageIndex].ExtensionBlockCount; i++) { ExtensionBlock* ep = &GifFile->SavedImages[ImageIndex].ExtensionBlocks[i]; if (ep->Function == GRAPHICS_EXT_FUNC_CODE) { return DGifExtensionToGCB( ep->ByteCount, (const GifByteType*)ep->Bytes, gcb); } } return GIF_ERROR; } #define CONTINUE_EXT_FUNC_CODE 0x00 // Signature was changed in v5.0 #define DGifOpenFileName(a, b) DGifOpenFileName(a) #endif // !LOCAL_GIF_PREREQ(5, 0) // Signature changed in v5.1 #if !LOCAL_GIF_PREREQ(5, 1) #define DGifCloseFile(a, b) DGifCloseFile(a) #endif static void GIFDisplayError(const GifFileType* const gif, int gif_error) { // libgif 4.2.0 has retired PrintGifError() and added GifErrorString(). #if LOCAL_GIF_PREREQ(4, 2) #if LOCAL_GIF_PREREQ(5, 0) const char* error_str = GifErrorString((gif == NULL) ? gif_error : gif->Error); #else const char* error_str = 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 int IsKeyFrameGIF(const GifImageDesc* prev_desc, int prev_dispose, const DecodedFrame* const prev_frame, int canvas_width, int canvas_height) { if (prev_frame == NULL) return 1; if (prev_dispose == DISPOSE_BACKGROUND) { if (IsFullFrame(prev_desc->Width, prev_desc->Height, canvas_width, canvas_height)) { return 1; } if (prev_frame->is_key_frame) return 1; } return 0; } static int GetTransparentIndexGIF(GifFileType* gif) { GraphicsControlBlock first_gcb; memset(&first_gcb, 0, sizeof(first_gcb)); DGifSavedExtensionToGCB(gif, 0, &first_gcb); return first_gcb.TransparentColor; } static uint32_t GetBackgroundColorGIF(GifFileType* gif) { const int transparent_index = GetTransparentIndexGIF(gif); const ColorMapObject* const color_map = gif->SColorMap; if (transparent_index != NO_TRANSPARENT_COLOR && gif->SBackGroundColor == transparent_index) { return 0x00000000; // Special case: transparent black. } else if (color_map == NULL || color_map->Colors == NULL || gif->SBackGroundColor >= color_map->ColorCount) { return 0xffffffff; // Invalid: assume white. } else { const GifColorType color = color_map->Colors[gif->SBackGroundColor]; return (0xff << 24) | (color.Red << 16) | (color.Green << 8) | (color.Blue << 0); } } // Find appropriate app extension and get loop count from the next extension. // We use Chrome's interpretation of the 'loop_count' semantics: // if not present -> loop once // if present and loop_count == 0, return 0 ('infinite'). // if present and loop_count != 0, it's the number of *extra* loops // so we need to return loop_count + 1 as total loop number. static uint32_t GetLoopCountGIF(const GifFileType* const gif) { int i; for (i = 0; i < gif->ImageCount; ++i) { const SavedImage* const image = &gif->SavedImages[i]; int j; for (j = 0; (j + 1) < image->ExtensionBlockCount; ++j) { const ExtensionBlock* const eb1 = image->ExtensionBlocks + j; const ExtensionBlock* const eb2 = image->ExtensionBlocks + j + 1; const char* const signature = (const char*)eb1->Bytes; const int signature_is_ok = (eb1->Function == APPLICATION_EXT_FUNC_CODE) && (eb1->ByteCount == 11) && (!memcmp(signature, "NETSCAPE2.0", 11) || !memcmp(signature, "ANIMEXTS1.0", 11)); if (signature_is_ok && eb2->Function == CONTINUE_EXT_FUNC_CODE && eb2->ByteCount >= 3 && eb2->Bytes[0] == 1) { const uint32_t extra_loop = ((uint32_t)(eb2->Bytes[2]) << 8) + ((uint32_t)(eb2->Bytes[1]) << 0); return (extra_loop > 0) ? extra_loop + 1 : 0; } } } return 1; // Default. } // Get duration of 'n'th frame in milliseconds. static int GetFrameDurationGIF(GifFileType* gif, int n) { GraphicsControlBlock gcb; memset(&gcb, 0, sizeof(gcb)); DGifSavedExtensionToGCB(gif, n, &gcb); return gcb.DelayTime * 10; } // Returns true if frame 'target' completely covers 'covered'. static int CoversFrameGIF(const GifImageDesc* const target, const GifImageDesc* const covered) { return target->Left <= covered->Left && covered->Left + covered->Width <= target->Left + target->Width && target->Top <= covered->Top && covered->Top + covered->Height <= target->Top + target->Height; } static void RemapPixelsGIF(const uint8_t* const src, const ColorMapObject* const cmap, int transparent_color, int len, uint8_t* dst) { int i; for (i = 0; i < len; ++i) { if (src[i] != transparent_color) { // If a pixel in the current frame is transparent, we don't modify it, so // that we can see-through the corresponding pixel from an earlier frame. const GifColorType c = cmap->Colors[src[i]]; dst[4 * i + 0] = c.Red; dst[4 * i + 1] = c.Green; dst[4 * i + 2] = c.Blue; dst[4 * i + 3] = 0xff; } } } static int ReadFrameGIF(const SavedImage* const gif_image, const ColorMapObject* cmap, int transparent_color, int out_stride, uint8_t* const dst) { const GifImageDesc* image_desc = &gif_image->ImageDesc; const uint8_t* in; uint8_t* out; int j; if (image_desc->ColorMap) cmap = image_desc->ColorMap; if (cmap == NULL || cmap->ColorCount != (1 << cmap->BitsPerPixel)) { fprintf(stderr, "Potentially corrupt color map.\n"); return 0; } in = (const uint8_t*)gif_image->RasterBits; out = dst + image_desc->Top * out_stride + image_desc->Left * kNumChannels; for (j = 0; j < image_desc->Height; ++j) { RemapPixelsGIF(in, cmap, transparent_color, image_desc->Width, out); in += image_desc->Width; out += out_stride; } return 1; } // Read animated GIF bitstream from 'filename' into 'AnimatedImage' struct. static int ReadAnimatedGIF(const char filename[], AnimatedImage* const image, int dump_frames, const char dump_folder[]) { uint32_t frame_count; uint32_t canvas_width, canvas_height; uint32_t i; int gif_error; GifFileType* gif; gif = DGifOpenFileName(filename, NULL); if (gif == NULL) { fprintf(stderr, "Could not read file: %s.\n", filename); return 0; } gif_error = DGifSlurp(gif); if (gif_error != GIF_OK) { fprintf(stderr, "Could not parse image: %s.\n", filename); GIFDisplayError(gif, gif_error); DGifCloseFile(gif, NULL); return 0; } // Animation properties. image->canvas_width = (uint32_t)gif->SWidth; image->canvas_height = (uint32_t)gif->SHeight; if (image->canvas_width > MAX_CANVAS_SIZE || image->canvas_height > MAX_CANVAS_SIZE) { fprintf(stderr, "Invalid canvas dimension: %d x %d\n", image->canvas_width, image->canvas_height); DGifCloseFile(gif, NULL); return 0; } image->loop_count = GetLoopCountGIF(gif); image->bgcolor = GetBackgroundColorGIF(gif); frame_count = (uint32_t)gif->ImageCount; if (frame_count == 0) { DGifCloseFile(gif, NULL); return 0; } if (image->canvas_width == 0 || image->canvas_height == 0) { image->canvas_width = gif->SavedImages[0].ImageDesc.Width; image->canvas_height = gif->SavedImages[0].ImageDesc.Height; gif->SavedImages[0].ImageDesc.Left = 0; gif->SavedImages[0].ImageDesc.Top = 0; if (image->canvas_width == 0 || image->canvas_height == 0) { fprintf(stderr, "Invalid canvas size in GIF.\n"); DGifCloseFile(gif, NULL); return 0; } } // Allocate frames. AllocateFrames(image, frame_count); canvas_width = image->canvas_width; canvas_height = image->canvas_height; // Decode and reconstruct frames. for (i = 0; i < frame_count; ++i) { const int canvas_width_in_bytes = canvas_width * kNumChannels; const SavedImage* const curr_gif_image = &gif->SavedImages[i]; GraphicsControlBlock curr_gcb; DecodedFrame* curr_frame; uint8_t* curr_rgba; memset(&curr_gcb, 0, sizeof(curr_gcb)); DGifSavedExtensionToGCB(gif, i, &curr_gcb); curr_frame = &image->frames[i]; curr_rgba = curr_frame->rgba; curr_frame->duration = GetFrameDurationGIF(gif, i); // Force frames with a small or no duration to 100ms to be consistent // with web browsers and other transcoding tools (like gif2webp itself). if (curr_frame->duration <= 10) curr_frame->duration = 100; if (i == 0) { // Initialize as transparent. curr_frame->is_key_frame = 1; ZeroFillCanvas(curr_rgba, canvas_width, canvas_height); } else { DecodedFrame* const prev_frame = &image->frames[i - 1]; const GifImageDesc* const prev_desc = &gif->SavedImages[i - 1].ImageDesc; GraphicsControlBlock prev_gcb; memset(&prev_gcb, 0, sizeof(prev_gcb)); DGifSavedExtensionToGCB(gif, i - 1, &prev_gcb); curr_frame->is_key_frame = IsKeyFrameGIF(prev_desc, prev_gcb.DisposalMode, prev_frame, canvas_width, canvas_height); if (curr_frame->is_key_frame) { // Initialize as transparent. ZeroFillCanvas(curr_rgba, canvas_width, canvas_height); } else { int prev_frame_disposed, curr_frame_opaque; int prev_frame_completely_covered; // Initialize with previous canvas. uint8_t* const prev_rgba = image->frames[i - 1].rgba; CopyCanvas(prev_rgba, curr_rgba, canvas_width, canvas_height); // Dispose previous frame rectangle. prev_frame_disposed = (prev_gcb.DisposalMode == DISPOSE_BACKGROUND || prev_gcb.DisposalMode == DISPOSE_PREVIOUS); curr_frame_opaque = (curr_gcb.TransparentColor == NO_TRANSPARENT_COLOR); prev_frame_completely_covered = curr_frame_opaque && CoversFrameGIF(&curr_gif_image->ImageDesc, prev_desc); if (prev_frame_disposed && !prev_frame_completely_covered) { switch (prev_gcb.DisposalMode) { case DISPOSE_BACKGROUND: { ZeroFillFrameRect(curr_rgba, canvas_width_in_bytes, prev_desc->Left, prev_desc->Top, prev_desc->Width, prev_desc->Height); break; } case DISPOSE_PREVIOUS: { int src_frame_num = i - 2; while (src_frame_num >= 0) { GraphicsControlBlock src_frame_gcb; memset(&src_frame_gcb, 0, sizeof(src_frame_gcb)); DGifSavedExtensionToGCB(gif, src_frame_num, &src_frame_gcb); if (src_frame_gcb.DisposalMode != DISPOSE_PREVIOUS) break; --src_frame_num; } if (src_frame_num >= 0) { // Restore pixels inside previous frame rectangle to // corresponding pixels in source canvas. uint8_t* const src_frame_rgba = image->frames[src_frame_num].rgba; CopyFrameRectangle(src_frame_rgba, curr_rgba, canvas_width_in_bytes, prev_desc->Left, prev_desc->Top, prev_desc->Width, prev_desc->Height); } else { // Source canvas doesn't exist. So clear previous frame // rectangle to background. ZeroFillFrameRect(curr_rgba, canvas_width_in_bytes, prev_desc->Left, prev_desc->Top, prev_desc->Width, prev_desc->Height); } break; } default: break; // Nothing to do. } } } } // Decode current frame. if (!ReadFrameGIF(curr_gif_image, gif->SColorMap, curr_gcb.TransparentColor, canvas_width_in_bytes, curr_rgba)) { DGifCloseFile(gif, NULL); return 0; } if (dump_frames) { if (!DumpFrame(filename, dump_folder, i, curr_rgba, canvas_width, canvas_height)) { DGifCloseFile(gif, NULL); return 0; } } } DGifCloseFile(gif, NULL); return 1; } #else static int IsGIF(const WebPData* const data) { (void)data; return 0; } static int ReadAnimatedGIF(const char filename[], AnimatedImage* const image, int dump_frames, const char dump_folder[]) { (void)filename; (void)image; (void)dump_frames; (void)dump_folder; fprintf(stderr, "GIF support not compiled. Please install the libgif-dev " "package before building.\n"); return 0; } #endif // WEBP_HAVE_GIF // ----------------------------------------------------------------------------- int ReadAnimatedImage(const char filename[], AnimatedImage* const image, int dump_frames, const char dump_folder[]) { int ok = 0; WebPData webp_data; WebPDataInit(&webp_data); memset(image, 0, sizeof(*image)); if (!ImgIoUtilReadFile(filename, &webp_data.bytes, &webp_data.size)) { fprintf(stderr, "Error reading file: %s\n", filename); return 0; } if (IsWebP(&webp_data)) { ok = ReadAnimatedWebP(filename, &webp_data, image, dump_frames, dump_folder); } else if (IsGIF(&webp_data)) { ok = ReadAnimatedGIF(filename, image, dump_frames, dump_folder); } else { fprintf(stderr, "Unknown file type: %s. Supported file types are WebP and GIF\n", filename); ok = 0; } if (!ok) ClearAnimatedImage(image); WebPDataClear(&webp_data); return ok; } static void Accumulate(double v1, double v2, double* const max_diff, double* const sse) { const double diff = fabs(v1 - v2); if (diff > *max_diff) *max_diff = diff; *sse += diff * diff; } void GetDiffAndPSNR(const uint8_t rgba1[], const uint8_t rgba2[], uint32_t width, uint32_t height, int premultiply, int* const max_diff, double* const psnr) { const uint32_t stride = width * kNumChannels; const int kAlphaChannel = kNumChannels - 1; double f_max_diff = 0.; double sse = 0.; uint32_t x, y; for (y = 0; y < height; ++y) { for (x = 0; x < stride; x += kNumChannels) { int k; const size_t offset = (size_t)y * stride + x; const int alpha1 = rgba1[offset + kAlphaChannel]; const int alpha2 = rgba2[offset + kAlphaChannel]; Accumulate(alpha1, alpha2, &f_max_diff, &sse); if (!premultiply) { for (k = 0; k < kAlphaChannel; ++k) { Accumulate(rgba1[offset + k], rgba2[offset + k], &f_max_diff, &sse); } } else { // premultiply R/G/B channels with alpha value for (k = 0; k < kAlphaChannel; ++k) { Accumulate(rgba1[offset + k] * alpha1 / 255., rgba2[offset + k] * alpha2 / 255., &f_max_diff, &sse); } } } } *max_diff = (int)f_max_diff; if (*max_diff == 0) { *psnr = 99.; // PSNR when images are identical. } else { sse /= stride * height; *psnr = 4.3429448 * log(255. * 255. / sse); } }