From 168f8b08bccf0d3e163879c7e39f045af0be7a08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Marjam=C3=A4ki?= Date: Sun, 3 May 2020 18:33:54 +0200 Subject: [PATCH] Add bug hunting test case for CVE-2019-13454 --- .../cve/CVE-2019-13454/expected.txt | 3 + test/bug-hunting/cve/CVE-2019-13454/layer.c | 2055 +++++++++++++++++ 2 files changed, 2058 insertions(+) create mode 100644 test/bug-hunting/cve/CVE-2019-13454/expected.txt create mode 100644 test/bug-hunting/cve/CVE-2019-13454/layer.c diff --git a/test/bug-hunting/cve/CVE-2019-13454/expected.txt b/test/bug-hunting/cve/CVE-2019-13454/expected.txt new file mode 100644 index 000000000..09bafadcc --- /dev/null +++ b/test/bug-hunting/cve/CVE-2019-13454/expected.txt @@ -0,0 +1,3 @@ +layer.c:1616:bughuntingDivByZero +layer.c:1617:bughuntingDivByZero + diff --git a/test/bug-hunting/cve/CVE-2019-13454/layer.c b/test/bug-hunting/cve/CVE-2019-13454/layer.c new file mode 100644 index 000000000..b520e9247 --- /dev/null +++ b/test/bug-hunting/cve/CVE-2019-13454/layer.c @@ -0,0 +1,2055 @@ +/* +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% % +% % +% L AAA Y Y EEEEE RRRR % +% L A A Y Y E R R % +% L AAAAA Y EEE RRRR % +% L A A Y E R R % +% LLLLL A A Y EEEEE R R % +% % +% MagickCore Image Layering Methods % +% % +% Software Design % +% Cristy % +% Anthony Thyssen % +% January 2006 % +% % +% % +% Copyright 1999-2019 ImageMagick Studio LLC, a non-profit organization % +% dedicated to making software imaging solutions freely available. % +% % +% You may not use this file except in compliance with the License. You may % +% obtain a copy of the License at % +% % +% https://imagemagick.org/script/license.php % +% % +% Unless required by applicable law or agreed to in writing, software % +% distributed under the License is distributed on an "AS IS" BASIS, % +% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. % +% See the License for the specific language governing permissions and % +% limitations under the License. % +% % +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% +*/ + +/* + Include declarations. +*/ +#include "MagickCore/studio.h" +#include "MagickCore/artifact.h" +#include "MagickCore/cache.h" +#include "MagickCore/channel.h" +#include "MagickCore/color.h" +#include "MagickCore/color-private.h" +#include "MagickCore/composite.h" +#include "MagickCore/effect.h" +#include "MagickCore/exception.h" +#include "MagickCore/exception-private.h" +#include "MagickCore/geometry.h" +#include "MagickCore/image.h" +#include "MagickCore/layer.h" +#include "MagickCore/list.h" +#include "MagickCore/memory_.h" +#include "MagickCore/monitor.h" +#include "MagickCore/monitor-private.h" +#include "MagickCore/option.h" +#include "MagickCore/pixel-accessor.h" +#include "MagickCore/property.h" +#include "MagickCore/profile.h" +#include "MagickCore/resource_.h" +#include "MagickCore/resize.h" +#include "MagickCore/statistic.h" +#include "MagickCore/string_.h" +#include "MagickCore/transform.h" + +/* +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% % +% % +% % ++ C l e a r B o u n d s % +% % +% % +% % +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% +% ClearBounds() Clear the area specified by the bounds in an image to +% transparency. This typically used to handle Background Disposal for the +% previous frame in an animation sequence. +% +% Warning: no bounds checks are performed, except for the null or missed +% image, for images that don't change. in all other cases bound must fall +% within the image. +% +% The format is: +% +% void ClearBounds(Image *image,RectangleInfo *bounds, +% ExceptionInfo *exception) +% +% A description of each parameter follows: +% +% o image: the image to had the area cleared in +% +% o bounds: the area to be clear within the imag image +% +% o exception: return any errors or warnings in this structure. +% +*/ +static void ClearBounds(Image *image,RectangleInfo *bounds, + ExceptionInfo *exception) +{ + ssize_t + y; + + if (bounds->x < 0) + return; + if (image->alpha_trait == UndefinedPixelTrait) + (void) SetImageAlphaChannel(image,OpaqueAlphaChannel,exception); + for (y=0; y < (ssize_t) bounds->height; y++) + { + register ssize_t + x; + + register Quantum + *magick_restrict q; + + q=GetAuthenticPixels(image,bounds->x,bounds->y+y,bounds->width,1,exception); + if (q == (Quantum *) NULL) + break; + for (x=0; x < (ssize_t) bounds->width; x++) + { + SetPixelAlpha(image,TransparentAlpha,q); + q+=GetPixelChannels(image); + } + if (SyncAuthenticPixels(image,exception) == MagickFalse) + break; + } +} + +/* +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% % +% % +% % ++ I s B o u n d s C l e a r e d % +% % +% % +% % +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% +% IsBoundsCleared() tests whether any pixel in the bounds given, gets cleared +% when going from the first image to the second image. This typically used +% to check if a proposed disposal method will work successfully to generate +% the second frame image from the first disposed form of the previous frame. +% +% Warning: no bounds checks are performed, except for the null or missed +% image, for images that don't change. in all other cases bound must fall +% within the image. +% +% The format is: +% +% MagickBooleanType IsBoundsCleared(const Image *image1, +% const Image *image2,RectangleInfo bounds,ExceptionInfo *exception) +% +% A description of each parameter follows: +% +% o image1, image 2: the images to check for cleared pixels +% +% o bounds: the area to be clear within the imag image +% +% o exception: return any errors or warnings in this structure. +% +*/ +static MagickBooleanType IsBoundsCleared(const Image *image1, + const Image *image2,RectangleInfo *bounds,ExceptionInfo *exception) +{ + register const Quantum + *p, + *q; + + register ssize_t + x; + + ssize_t + y; + + if (bounds->x < 0) + return(MagickFalse); + for (y=0; y < (ssize_t) bounds->height; y++) + { + p=GetVirtualPixels(image1,bounds->x,bounds->y+y,bounds->width,1,exception); + q=GetVirtualPixels(image2,bounds->x,bounds->y+y,bounds->width,1,exception); + if ((p == (const Quantum *) NULL) || (q == (Quantum *) NULL)) + break; + for (x=0; x < (ssize_t) bounds->width; x++) + { + if ((GetPixelAlpha(image1,p) >= (Quantum) (QuantumRange/2)) && + (GetPixelAlpha(image2,q) < (Quantum) (QuantumRange/2))) + break; + p+=GetPixelChannels(image1); + q+=GetPixelChannels(image2); + } + if (x < (ssize_t) bounds->width) + break; + } + return(y < (ssize_t) bounds->height ? MagickTrue : MagickFalse); +} + +/* +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% % +% % +% % +% C o a l e s c e I m a g e s % +% % +% % +% % +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% +% CoalesceImages() composites a set of images while respecting any page +% offsets and disposal methods. GIF, MIFF, and MNG animation sequences +% typically start with an image background and each subsequent image +% varies in size and offset. A new image sequence is returned with all +% images the same size as the first images virtual canvas and composited +% with the next image in the sequence. +% +% The format of the CoalesceImages method is: +% +% Image *CoalesceImages(Image *image,ExceptionInfo *exception) +% +% A description of each parameter follows: +% +% o image: the image sequence. +% +% o exception: return any errors or warnings in this structure. +% +*/ +MagickExport Image *CoalesceImages(const Image *image,ExceptionInfo *exception) +{ + Image + *coalesce_image, + *dispose_image, + *previous; + + register Image + *next; + + RectangleInfo + bounds; + + /* + Coalesce the image sequence. + */ + assert(image != (Image *) NULL); + assert(image->signature == MagickCoreSignature); + if (image->debug != MagickFalse) + (void) LogMagickEvent(TraceEvent,GetMagickModule(),"%s",image->filename); + assert(exception != (ExceptionInfo *) NULL); + assert(exception->signature == MagickCoreSignature); + next=GetFirstImageInList(image); + bounds=next->page; + if (bounds.width == 0) + { + bounds.width=next->columns; + if (bounds.x > 0) + bounds.width+=bounds.x; + } + if (bounds.height == 0) + { + bounds.height=next->rows; + if (bounds.y > 0) + bounds.height+=bounds.y; + } + bounds.x=0; + bounds.y=0; + coalesce_image=CloneImage(next,bounds.width,bounds.height,MagickTrue, + exception); + if (coalesce_image == (Image *) NULL) + return((Image *) NULL); + coalesce_image->background_color.alpha=(MagickRealType) TransparentAlpha; + (void) SetImageBackgroundColor(coalesce_image,exception); + coalesce_image->alpha_trait=next->alpha_trait; + coalesce_image->page=bounds; + coalesce_image->dispose=NoneDispose; + /* + Coalesce rest of the images. + */ + dispose_image=CloneImage(coalesce_image,0,0,MagickTrue,exception); + (void) CompositeImage(coalesce_image,next,CopyCompositeOp,MagickTrue, + next->page.x,next->page.y,exception); + next=GetNextImageInList(next); + for ( ; next != (Image *) NULL; next=GetNextImageInList(next)) + { + /* + Determine the bounds that was overlaid in the previous image. + */ + previous=GetPreviousImageInList(next); + bounds=previous->page; + bounds.width=previous->columns; + bounds.height=previous->rows; + if (bounds.x < 0) + { + bounds.width+=bounds.x; + bounds.x=0; + } + if ((ssize_t) (bounds.x+bounds.width) > (ssize_t) coalesce_image->columns) + bounds.width=coalesce_image->columns-bounds.x; + if (bounds.y < 0) + { + bounds.height+=bounds.y; + bounds.y=0; + } + if ((ssize_t) (bounds.y+bounds.height) > (ssize_t) coalesce_image->rows) + bounds.height=coalesce_image->rows-bounds.y; + /* + Replace the dispose image with the new coalesced image. + */ + if (GetPreviousImageInList(next)->dispose != PreviousDispose) + { + dispose_image=DestroyImage(dispose_image); + dispose_image=CloneImage(coalesce_image,0,0,MagickTrue,exception); + if (dispose_image == (Image *) NULL) + { + coalesce_image=DestroyImageList(coalesce_image); + return((Image *) NULL); + } + } + /* + Clear the overlaid area of the coalesced bounds for background disposal + */ + if (next->previous->dispose == BackgroundDispose) + ClearBounds(dispose_image,&bounds,exception); + /* + Next image is the dispose image, overlaid with next frame in sequence. + */ + coalesce_image->next=CloneImage(dispose_image,0,0,MagickTrue,exception); + coalesce_image->next->previous=coalesce_image; + previous=coalesce_image; + coalesce_image=GetNextImageInList(coalesce_image); + (void) CompositeImage(coalesce_image,next, + next->alpha_trait != UndefinedPixelTrait ? OverCompositeOp : CopyCompositeOp, + MagickTrue,next->page.x,next->page.y,exception); + (void) CloneImageProfiles(coalesce_image,next); + (void) CloneImageProperties(coalesce_image,next); + (void) CloneImageArtifacts(coalesce_image,next); + coalesce_image->page=previous->page; + /* + If a pixel goes opaque to transparent, use background dispose. + */ + if (IsBoundsCleared(previous,coalesce_image,&bounds,exception) != MagickFalse) + coalesce_image->dispose=BackgroundDispose; + else + coalesce_image->dispose=NoneDispose; + previous->dispose=coalesce_image->dispose; + } + dispose_image=DestroyImage(dispose_image); + return(GetFirstImageInList(coalesce_image)); +} + +/* +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% % +% % +% % +% D i s p o s e I m a g e s % +% % +% % +% % +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% +% DisposeImages() returns the coalesced frames of a GIF animation as it would +% appear after the GIF dispose method of that frame has been applied. That is +% it returned the appearance of each frame before the next is overlaid. +% +% The format of the DisposeImages method is: +% +% Image *DisposeImages(Image *image,ExceptionInfo *exception) +% +% A description of each parameter follows: +% +% o images: the image sequence. +% +% o exception: return any errors or warnings in this structure. +% +*/ +MagickExport Image *DisposeImages(const Image *images,ExceptionInfo *exception) +{ + Image + *dispose_image, + *dispose_images; + + RectangleInfo + bounds; + + register Image + *image, + *next; + + /* + Run the image through the animation sequence + */ + assert(images != (Image *) NULL); + assert(images->signature == MagickCoreSignature); + if (images->debug != MagickFalse) + (void) LogMagickEvent(TraceEvent,GetMagickModule(),"%s",images->filename); + assert(exception != (ExceptionInfo *) NULL); + assert(exception->signature == MagickCoreSignature); + image=GetFirstImageInList(images); + dispose_image=CloneImage(image,image->page.width,image->page.height, + MagickTrue,exception); + if (dispose_image == (Image *) NULL) + return((Image *) NULL); + dispose_image->page=image->page; + dispose_image->page.x=0; + dispose_image->page.y=0; + dispose_image->dispose=NoneDispose; + dispose_image->background_color.alpha=(MagickRealType) TransparentAlpha; + (void) SetImageBackgroundColor(dispose_image,exception); + dispose_images=NewImageList(); + for (next=image; image != (Image *) NULL; image=GetNextImageInList(image)) + { + Image + *current_image; + + /* + Overlay this frame's image over the previous disposal image. + */ + current_image=CloneImage(dispose_image,0,0,MagickTrue,exception); + if (current_image == (Image *) NULL) + { + dispose_images=DestroyImageList(dispose_images); + dispose_image=DestroyImage(dispose_image); + return((Image *) NULL); + } + (void) CompositeImage(current_image,next, + next->alpha_trait != UndefinedPixelTrait ? OverCompositeOp : CopyCompositeOp, + MagickTrue,next->page.x,next->page.y,exception); + /* + Handle Background dispose: image is displayed for the delay period. + */ + if (next->dispose == BackgroundDispose) + { + bounds=next->page; + bounds.width=next->columns; + bounds.height=next->rows; + if (bounds.x < 0) + { + bounds.width+=bounds.x; + bounds.x=0; + } + if ((ssize_t) (bounds.x+bounds.width) > (ssize_t) current_image->columns) + bounds.width=current_image->columns-bounds.x; + if (bounds.y < 0) + { + bounds.height+=bounds.y; + bounds.y=0; + } + if ((ssize_t) (bounds.y+bounds.height) > (ssize_t) current_image->rows) + bounds.height=current_image->rows-bounds.y; + ClearBounds(current_image,&bounds,exception); + } + /* + Select the appropriate previous/disposed image. + */ + if (next->dispose == PreviousDispose) + current_image=DestroyImage(current_image); + else + { + dispose_image=DestroyImage(dispose_image); + dispose_image=current_image; + current_image=(Image *) NULL; + } + /* + Save the dispose image just calculated for return. + */ + { + Image + *dispose; + + dispose=CloneImage(dispose_image,0,0,MagickTrue,exception); + if (dispose == (Image *) NULL) + { + dispose_images=DestroyImageList(dispose_images); + dispose_image=DestroyImage(dispose_image); + return((Image *) NULL); + } + (void) CloneImageProfiles(dispose,next); + (void) CloneImageProperties(dispose,next); + (void) CloneImageArtifacts(dispose,next); + dispose->page.x=0; + dispose->page.y=0; + dispose->dispose=next->dispose; + AppendImageToList(&dispose_images,dispose); + } + } + dispose_image=DestroyImage(dispose_image); + return(GetFirstImageInList(dispose_images)); +} + +/* +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% % +% % +% % ++ C o m p a r e P i x e l s % +% % +% % +% % +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% +% ComparePixels() Compare the two pixels and return true if the pixels +% differ according to the given LayerType comparision method. +% +% This currently only used internally by CompareImagesBounds(). It is +% doubtful that this sub-routine will be useful outside this module. +% +% The format of the ComparePixels method is: +% +% MagickBooleanType *ComparePixels(const LayerMethod method, +% const PixelInfo *p,const PixelInfo *q) +% +% A description of each parameter follows: +% +% o method: What differences to look for. Must be one of +% CompareAnyLayer, CompareClearLayer, CompareOverlayLayer. +% +% o p, q: the pixels to test for appropriate differences. +% +*/ + +static MagickBooleanType ComparePixels(const LayerMethod method, + const PixelInfo *p,const PixelInfo *q) +{ + double + o1, + o2; + + /* + Any change in pixel values + */ + if (method == CompareAnyLayer) + return((MagickBooleanType)(IsFuzzyEquivalencePixelInfo(p,q) == MagickFalse)); + + o1 = (p->alpha_trait != UndefinedPixelTrait) ? p->alpha : OpaqueAlpha; + o2 = (q->alpha_trait != UndefinedPixelTrait) ? q->alpha : OpaqueAlpha; + /* + Pixel goes from opaque to transprency. + */ + if (method == CompareClearLayer) + return((MagickBooleanType) ( (o1 >= ((double) QuantumRange/2.0)) && + (o2 < ((double) QuantumRange/2.0)) ) ); + /* + Overlay would change first pixel by second. + */ + if (method == CompareOverlayLayer) + { + if (o2 < ((double) QuantumRange/2.0)) + return MagickFalse; + return((MagickBooleanType) (IsFuzzyEquivalencePixelInfo(p,q) == MagickFalse)); + } + return(MagickFalse); +} + + +/* +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% % +% % +% % ++ C o m p a r e I m a g e B o u n d s % +% % +% % +% % +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% +% CompareImagesBounds() Given two images return the smallest rectangular area +% by which the two images differ, accourding to the given 'Compare...' +% layer method. +% +% This currently only used internally in this module, but may eventually +% be used by other modules. +% +% The format of the CompareImagesBounds method is: +% +% RectangleInfo *CompareImagesBounds(const LayerMethod method, +% const Image *image1,const Image *image2,ExceptionInfo *exception) +% +% A description of each parameter follows: +% +% o method: What differences to look for. Must be one of CompareAnyLayer, +% CompareClearLayer, CompareOverlayLayer. +% +% o image1, image2: the two images to compare. +% +% o exception: return any errors or warnings in this structure. +% +*/ + +static RectangleInfo CompareImagesBounds(const Image *image1, + const Image *image2,const LayerMethod method,ExceptionInfo *exception) +{ + RectangleInfo + bounds; + + PixelInfo + pixel1, + pixel2; + + register const Quantum + *p, + *q; + + register ssize_t + x; + + ssize_t + y; + + /* + Set bounding box of the differences between images. + */ + GetPixelInfo(image1,&pixel1); + GetPixelInfo(image2,&pixel2); + for (x=0; x < (ssize_t) image1->columns; x++) + { + p=GetVirtualPixels(image1,x,0,1,image1->rows,exception); + q=GetVirtualPixels(image2,x,0,1,image2->rows,exception); + if ((p == (const Quantum *) NULL) || (q == (Quantum *) NULL)) + break; + for (y=0; y < (ssize_t) image1->rows; y++) + { + GetPixelInfoPixel(image1,p,&pixel1); + GetPixelInfoPixel(image2,q,&pixel2); + if (ComparePixels(method,&pixel1,&pixel2)) + break; + p+=GetPixelChannels(image1); + q+=GetPixelChannels(image2); + } + if (y < (ssize_t) image1->rows) + break; + } + if (x >= (ssize_t) image1->columns) + { + /* + Images are identical, return a null image. + */ + bounds.x=-1; + bounds.y=-1; + bounds.width=1; + bounds.height=1; + return(bounds); + } + bounds.x=x; + for (x=(ssize_t) image1->columns-1; x >= 0; x--) + { + p=GetVirtualPixels(image1,x,0,1,image1->rows,exception); + q=GetVirtualPixels(image2,x,0,1,image2->rows,exception); + if ((p == (const Quantum *) NULL) || (q == (Quantum *) NULL)) + break; + for (y=0; y < (ssize_t) image1->rows; y++) + { + GetPixelInfoPixel(image1,p,&pixel1); + GetPixelInfoPixel(image2,q,&pixel2); + if (ComparePixels(method,&pixel1,&pixel2)) + break; + p+=GetPixelChannels(image1); + q+=GetPixelChannels(image2); + } + if (y < (ssize_t) image1->rows) + break; + } + bounds.width=(size_t) (x-bounds.x+1); + for (y=0; y < (ssize_t) image1->rows; y++) + { + p=GetVirtualPixels(image1,0,y,image1->columns,1,exception); + q=GetVirtualPixels(image2,0,y,image2->columns,1,exception); + if ((p == (const Quantum *) NULL) || (q == (Quantum *) NULL)) + break; + for (x=0; x < (ssize_t) image1->columns; x++) + { + GetPixelInfoPixel(image1,p,&pixel1); + GetPixelInfoPixel(image2,q,&pixel2); + if (ComparePixels(method,&pixel1,&pixel2)) + break; + p+=GetPixelChannels(image1); + q+=GetPixelChannels(image2); + } + if (x < (ssize_t) image1->columns) + break; + } + bounds.y=y; + for (y=(ssize_t) image1->rows-1; y >= 0; y--) + { + p=GetVirtualPixels(image1,0,y,image1->columns,1,exception); + q=GetVirtualPixels(image2,0,y,image2->columns,1,exception); + if ((p == (const Quantum *) NULL) || (q == (Quantum *) NULL)) + break; + for (x=0; x < (ssize_t) image1->columns; x++) + { + GetPixelInfoPixel(image1,p,&pixel1); + GetPixelInfoPixel(image2,q,&pixel2); + if (ComparePixels(method,&pixel1,&pixel2)) + break; + p+=GetPixelChannels(image1); + q+=GetPixelChannels(image2); + } + if (x < (ssize_t) image1->columns) + break; + } + bounds.height=(size_t) (y-bounds.y+1); + return(bounds); +} + +/* +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% % +% % +% % +% C o m p a r e I m a g e L a y e r s % +% % +% % +% % +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% +% CompareImagesLayers() compares each image with the next in a sequence and +% returns the minimum bounding region of all the pixel differences (of the +% LayerMethod specified) it discovers. +% +% Images do NOT have to be the same size, though it is best that all the +% images are 'coalesced' (images are all the same size, on a flattened +% canvas, so as to represent exactly how an specific frame should look). +% +% No GIF dispose methods are applied, so GIF animations must be coalesced +% before applying this image operator to find differences to them. +% +% The format of the CompareImagesLayers method is: +% +% Image *CompareImagesLayers(const Image *images, +% const LayerMethod method,ExceptionInfo *exception) +% +% A description of each parameter follows: +% +% o image: the image. +% +% o method: the layers type to compare images with. Must be one of... +% CompareAnyLayer, CompareClearLayer, CompareOverlayLayer. +% +% o exception: return any errors or warnings in this structure. +% +*/ + +MagickExport Image *CompareImagesLayers(const Image *image, + const LayerMethod method,ExceptionInfo *exception) +{ + Image + *image_a, + *image_b, + *layers; + + RectangleInfo + *bounds; + + register const Image + *next; + + register ssize_t + i; + + assert(image != (const Image *) NULL); + assert(image->signature == MagickCoreSignature); + if (image->debug != MagickFalse) + (void) LogMagickEvent(TraceEvent,GetMagickModule(),"%s",image->filename); + assert(exception != (ExceptionInfo *) NULL); + assert(exception->signature == MagickCoreSignature); + assert((method == CompareAnyLayer) || + (method == CompareClearLayer) || + (method == CompareOverlayLayer)); + /* + Allocate bounds memory. + */ + next=GetFirstImageInList(image); + bounds=(RectangleInfo *) AcquireQuantumMemory((size_t) + GetImageListLength(next),sizeof(*bounds)); + if (bounds == (RectangleInfo *) NULL) + ThrowImageException(ResourceLimitError,"MemoryAllocationFailed"); + /* + Set up first comparision images. + */ + image_a=CloneImage(next,next->page.width,next->page.height, + MagickTrue,exception); + if (image_a == (Image *) NULL) + { + bounds=(RectangleInfo *) RelinquishMagickMemory(bounds); + return((Image *) NULL); + } + image_a->background_color.alpha=(MagickRealType) TransparentAlpha; + (void) SetImageBackgroundColor(image_a,exception); + image_a->page=next->page; + image_a->page.x=0; + image_a->page.y=0; + (void) CompositeImage(image_a,next,CopyCompositeOp,MagickTrue,next->page.x, + next->page.y,exception); + /* + Compute the bounding box of changes for the later images + */ + i=0; + next=GetNextImageInList(next); + for ( ; next != (const Image *) NULL; next=GetNextImageInList(next)) + { + image_b=CloneImage(image_a,0,0,MagickTrue,exception); + if (image_b == (Image *) NULL) + { + image_a=DestroyImage(image_a); + bounds=(RectangleInfo *) RelinquishMagickMemory(bounds); + return((Image *) NULL); + } + (void) CompositeImage(image_a,next,CopyCompositeOp,MagickTrue,next->page.x, + next->page.y,exception); + bounds[i]=CompareImagesBounds(image_b,image_a,method,exception); + image_b=DestroyImage(image_b); + i++; + } + image_a=DestroyImage(image_a); + /* + Clone first image in sequence. + */ + next=GetFirstImageInList(image); + layers=CloneImage(next,0,0,MagickTrue,exception); + if (layers == (Image *) NULL) + { + bounds=(RectangleInfo *) RelinquishMagickMemory(bounds); + return((Image *) NULL); + } + /* + Deconstruct the image sequence. + */ + i=0; + next=GetNextImageInList(next); + for ( ; next != (const Image *) NULL; next=GetNextImageInList(next)) + { + if ((bounds[i].x == -1) && (bounds[i].y == -1) && + (bounds[i].width == 1) && (bounds[i].height == 1)) + { + /* + An empty frame is returned from CompareImageBounds(), which means the + current frame is identical to the previous frame. + */ + i++; + continue; + } + image_a=CloneImage(next,0,0,MagickTrue,exception); + if (image_a == (Image *) NULL) + break; + image_b=CropImage(image_a,&bounds[i],exception); + image_a=DestroyImage(image_a); + if (image_b == (Image *) NULL) + break; + AppendImageToList(&layers,image_b); + i++; + } + bounds=(RectangleInfo *) RelinquishMagickMemory(bounds); + if (next != (Image *) NULL) + { + layers=DestroyImageList(layers); + return((Image *) NULL); + } + return(GetFirstImageInList(layers)); +} + +/* +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% % +% % +% % ++ O p t i m i z e L a y e r F r a m e s % +% % +% % +% % +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% +% OptimizeLayerFrames() takes a coalesced GIF animation, and compares each +% frame against the three different 'disposal' forms of the previous frame. +% From this it then attempts to select the smallest cropped image and +% disposal method needed to reproduce the resulting image. +% +% Note that this not easy, and may require the expansion of the bounds +% of previous frame, simply clear pixels for the next animation frame to +% transparency according to the selected dispose method. +% +% The format of the OptimizeLayerFrames method is: +% +% Image *OptimizeLayerFrames(const Image *image, +% const LayerMethod method,ExceptionInfo *exception) +% +% A description of each parameter follows: +% +% o image: the image. +% +% o method: the layers technique to optimize with. Must be one of... +% OptimizeImageLayer, or OptimizePlusLayer. The Plus form allows +% the addition of extra 'zero delay' frames to clear pixels from +% the previous frame, and the removal of frames that done change, +% merging the delay times together. +% +% o exception: return any errors or warnings in this structure. +% +*/ +/* + Define a 'fake' dispose method where the frame is duplicated, (for + OptimizePlusLayer) with a extra zero time delay frame which does a + BackgroundDisposal to clear the pixels that need to be cleared. +*/ +#define DupDispose ((DisposeType)9) +/* + Another 'fake' dispose method used to removed frames that don't change. +*/ +#define DelDispose ((DisposeType)8) + +#define DEBUG_OPT_FRAME 0 + +static Image *OptimizeLayerFrames(const Image *image,const LayerMethod method, + ExceptionInfo *exception) +{ + ExceptionInfo + *sans_exception; + + Image + *prev_image, + *dup_image, + *bgnd_image, + *optimized_image; + + RectangleInfo + try_bounds, + bgnd_bounds, + dup_bounds, + *bounds; + + MagickBooleanType + add_frames, + try_cleared, + cleared; + + DisposeType + *disposals; + + register const Image + *curr; + + register ssize_t + i; + + assert(image != (const Image *) NULL); + assert(image->signature == MagickCoreSignature); + if (image->debug != MagickFalse) + (void) LogMagickEvent(TraceEvent,GetMagickModule(),"%s",image->filename); + assert(exception != (ExceptionInfo *) NULL); + assert(exception->signature == MagickCoreSignature); + assert(method == OptimizeLayer || + method == OptimizeImageLayer || + method == OptimizePlusLayer); + /* + Are we allowed to add/remove frames from animation? + */ + add_frames=method == OptimizePlusLayer ? MagickTrue : MagickFalse; + /* + Ensure all the images are the same size. + */ + curr=GetFirstImageInList(image); + for (; curr != (Image *) NULL; curr=GetNextImageInList(curr)) + { + if ((curr->columns != image->columns) || (curr->rows != image->rows)) + ThrowImageException(OptionError,"ImagesAreNotTheSameSize"); + + if ((curr->page.x != 0) || (curr->page.y != 0) || + (curr->page.width != image->page.width) || + (curr->page.height != image->page.height)) + ThrowImageException(OptionError,"ImagePagesAreNotCoalesced"); + } + /* + Allocate memory (times 2 if we allow the use of frame duplications) + */ + curr=GetFirstImageInList(image); + bounds=(RectangleInfo *) AcquireQuantumMemory((size_t) + GetImageListLength(curr),(add_frames != MagickFalse ? 2UL : 1UL)* + sizeof(*bounds)); + if (bounds == (RectangleInfo *) NULL) + ThrowImageException(ResourceLimitError,"MemoryAllocationFailed"); + disposals=(DisposeType *) AcquireQuantumMemory((size_t) + GetImageListLength(image),(add_frames != MagickFalse ? 2UL : 1UL)* + sizeof(*disposals)); + if (disposals == (DisposeType *) NULL) + { + bounds=(RectangleInfo *) RelinquishMagickMemory(bounds); + ThrowImageException(ResourceLimitError,"MemoryAllocationFailed"); + } + /* + Initialise Previous Image as fully transparent + */ + prev_image=CloneImage(curr,curr->columns,curr->rows,MagickTrue,exception); + if (prev_image == (Image *) NULL) + { + bounds=(RectangleInfo *) RelinquishMagickMemory(bounds); + disposals=(DisposeType *) RelinquishMagickMemory(disposals); + return((Image *) NULL); + } + prev_image->page=curr->page; /* ERROR: <-- should not be need, but is! */ + prev_image->page.x=0; + prev_image->page.y=0; + prev_image->dispose=NoneDispose; + prev_image->background_color.alpha_trait=BlendPixelTrait; + prev_image->background_color.alpha=(MagickRealType) TransparentAlpha; + (void) SetImageBackgroundColor(prev_image,exception); + /* + Figure out the area of overlay of the first frame + No pixel could be cleared as all pixels are already cleared. + */ +#if DEBUG_OPT_FRAME + i=0; + (void) FormatLocaleFile(stderr,"frame %.20g :-\n",(double) i); +#endif + disposals[0]=NoneDispose; + bounds[0]=CompareImagesBounds(prev_image,curr,CompareAnyLayer,exception); +#if DEBUG_OPT_FRAME + (void) FormatLocaleFile(stderr, "overlay: %.20gx%.20g%+.20g%+.20g\n\n", + (double) bounds[i].width,(double) bounds[i].height, + (double) bounds[i].x,(double) bounds[i].y ); +#endif + /* + Compute the bounding box of changes for each pair of images. + */ + i=1; + bgnd_image=(Image *) NULL; + dup_image=(Image *) NULL; + dup_bounds.width=0; + dup_bounds.height=0; + dup_bounds.x=0; + dup_bounds.y=0; + curr=GetNextImageInList(curr); + for ( ; curr != (const Image *) NULL; curr=GetNextImageInList(curr)) + { +#if DEBUG_OPT_FRAME + (void) FormatLocaleFile(stderr,"frame %.20g :-\n",(double) i); +#endif + /* + Assume none disposal is the best + */ + bounds[i]=CompareImagesBounds(curr->previous,curr,CompareAnyLayer,exception); + cleared=IsBoundsCleared(curr->previous,curr,&bounds[i],exception); + disposals[i-1]=NoneDispose; +#if DEBUG_OPT_FRAME + (void) FormatLocaleFile(stderr, "overlay: %.20gx%.20g%+.20g%+.20g%s%s\n", + (double) bounds[i].width,(double) bounds[i].height, + (double) bounds[i].x,(double) bounds[i].y, + bounds[i].x < 0?" (unchanged)":"", + cleared?" (pixels cleared)":""); +#endif + if ( bounds[i].x < 0 ) { + /* + Image frame is exactly the same as the previous frame! + If not adding frames leave it to be cropped down to a null image. + Otherwise mark previous image for deleted, transfering its crop bounds + to the current image. + */ + if ( add_frames && i>=2 ) { + disposals[i-1]=DelDispose; + disposals[i]=NoneDispose; + bounds[i]=bounds[i-1]; + i++; + continue; + } + } + else + { + /* + Compare a none disposal against a previous disposal + */ + try_bounds=CompareImagesBounds(prev_image,curr,CompareAnyLayer,exception); + try_cleared=IsBoundsCleared(prev_image,curr,&try_bounds,exception); +#if DEBUG_OPT_FRAME + (void) FormatLocaleFile(stderr, "test_prev: %.20gx%.20g%+.20g%+.20g%s\n", + (double) try_bounds.width,(double) try_bounds.height, + (double) try_bounds.x,(double) try_bounds.y, + try_cleared?" (pixels were cleared)":""); +#endif + if ( (!try_cleared && cleared ) || + try_bounds.width * try_bounds.height + < bounds[i].width * bounds[i].height ) + { + cleared=try_cleared; + bounds[i]=try_bounds; + disposals[i-1]=PreviousDispose; +#if DEBUG_OPT_FRAME + (void) FormatLocaleFile(stderr,"previous: accepted\n"); + } else { + (void) FormatLocaleFile(stderr,"previous: rejected\n"); +#endif + } + + /* + If we are allowed lets try a complex frame duplication. + It is useless if the previous image already clears pixels correctly. + This method will always clear all the pixels that need to be cleared. + */ + dup_bounds.width=dup_bounds.height=0; /* no dup, no pixel added */ + if ( add_frames ) + { + dup_image=CloneImage(curr->previous,0,0,MagickTrue,exception); + if (dup_image == (Image *) NULL) + { + bounds=(RectangleInfo *) RelinquishMagickMemory(bounds); + disposals=(DisposeType *) RelinquishMagickMemory(disposals); + prev_image=DestroyImage(prev_image); + return((Image *) NULL); + } + dup_bounds=CompareImagesBounds(dup_image,curr,CompareClearLayer,exception); + ClearBounds(dup_image,&dup_bounds,exception); + try_bounds=CompareImagesBounds(dup_image,curr,CompareAnyLayer,exception); + if ( cleared || + dup_bounds.width*dup_bounds.height + +try_bounds.width*try_bounds.height + < bounds[i].width * bounds[i].height ) + { + cleared=MagickFalse; + bounds[i]=try_bounds; + disposals[i-1]=DupDispose; + /* to be finalised later, if found to be optimial */ + } + else + dup_bounds.width=dup_bounds.height=0; + } + /* + Now compare against a simple background disposal + */ + bgnd_image=CloneImage(curr->previous,0,0,MagickTrue,exception); + if (bgnd_image == (Image *) NULL) + { + bounds=(RectangleInfo *) RelinquishMagickMemory(bounds); + disposals=(DisposeType *) RelinquishMagickMemory(disposals); + prev_image=DestroyImage(prev_image); + if ( dup_image != (Image *) NULL) + dup_image=DestroyImage(dup_image); + return((Image *) NULL); + } + bgnd_bounds=bounds[i-1]; /* interum bounds of the previous image */ + ClearBounds(bgnd_image,&bgnd_bounds,exception); + try_bounds=CompareImagesBounds(bgnd_image,curr,CompareAnyLayer,exception); + try_cleared=IsBoundsCleared(bgnd_image,curr,&try_bounds,exception); +#if DEBUG_OPT_FRAME + (void) FormatLocaleFile(stderr, "background: %s\n", + try_cleared?"(pixels cleared)":""); +#endif + if ( try_cleared ) + { + /* + Straight background disposal failed to clear pixels needed! + Lets try expanding the disposal area of the previous frame, to + include the pixels that are cleared. This guaranteed + to work, though may not be the most optimized solution. + */ + try_bounds=CompareImagesBounds(curr->previous,curr,CompareClearLayer,exception); +#if DEBUG_OPT_FRAME + (void) FormatLocaleFile(stderr, "expand_clear: %.20gx%.20g%+.20g%+.20g%s\n", + (double) try_bounds.width,(double) try_bounds.height, + (double) try_bounds.x,(double) try_bounds.y, + try_bounds.x<0?" (no expand nessary)":""); +#endif + if ( bgnd_bounds.x < 0 ) + bgnd_bounds = try_bounds; + else + { +#if DEBUG_OPT_FRAME + (void) FormatLocaleFile(stderr, "expand_bgnd: %.20gx%.20g%+.20g%+.20g\n", + (double) bgnd_bounds.width,(double) bgnd_bounds.height, + (double) bgnd_bounds.x,(double) bgnd_bounds.y ); +#endif + if ( try_bounds.x < bgnd_bounds.x ) + { + bgnd_bounds.width+= bgnd_bounds.x-try_bounds.x; + if ( bgnd_bounds.width < try_bounds.width ) + bgnd_bounds.width = try_bounds.width; + bgnd_bounds.x = try_bounds.x; + } + else + { + try_bounds.width += try_bounds.x - bgnd_bounds.x; + if ( bgnd_bounds.width < try_bounds.width ) + bgnd_bounds.width = try_bounds.width; + } + if ( try_bounds.y < bgnd_bounds.y ) + { + bgnd_bounds.height += bgnd_bounds.y - try_bounds.y; + if ( bgnd_bounds.height < try_bounds.height ) + bgnd_bounds.height = try_bounds.height; + bgnd_bounds.y = try_bounds.y; + } + else + { + try_bounds.height += try_bounds.y - bgnd_bounds.y; + if ( bgnd_bounds.height < try_bounds.height ) + bgnd_bounds.height = try_bounds.height; + } +#if DEBUG_OPT_FRAME + (void) FormatLocaleFile(stderr, " to : %.20gx%.20g%+.20g%+.20g\n", + (double) bgnd_bounds.width,(double) bgnd_bounds.height, + (double) bgnd_bounds.x,(double) bgnd_bounds.y ); +#endif + } + ClearBounds(bgnd_image,&bgnd_bounds,exception); +#if DEBUG_OPT_FRAME +/* Something strange is happening with a specific animation + * CompareAnyLayers (normal method) and CompareClearLayers returns the whole + * image, which is not posibly correct! As verified by previous tests. + * Something changed beyond the bgnd_bounds clearing. But without being able + * to see, or writet he image at this point it is hard to tell what is wrong! + * Only CompareOverlay seemed to return something sensible. + */ + try_bounds=CompareImagesBounds(bgnd_image,curr,CompareClearLayer,exception); + (void) FormatLocaleFile(stderr, "expand_ctst: %.20gx%.20g%+.20g%+.20g\n", + (double) try_bounds.width,(double) try_bounds.height, + (double) try_bounds.x,(double) try_bounds.y ); + try_bounds=CompareImagesBounds(bgnd_image,curr,CompareAnyLayer,exception); + try_cleared=IsBoundsCleared(bgnd_image,curr,&try_bounds,exception); + (void) FormatLocaleFile(stderr, "expand_any : %.20gx%.20g%+.20g%+.20g%s\n", + (double) try_bounds.width,(double) try_bounds.height, + (double) try_bounds.x,(double) try_bounds.y, + try_cleared?" (pixels cleared)":""); +#endif + try_bounds=CompareImagesBounds(bgnd_image,curr,CompareOverlayLayer,exception); +#if DEBUG_OPT_FRAME + try_cleared=IsBoundsCleared(bgnd_image,curr,&try_bounds,exception); + (void) FormatLocaleFile(stderr, "expand_test: %.20gx%.20g%+.20g%+.20g%s\n", + (double) try_bounds.width,(double) try_bounds.height, + (double) try_bounds.x,(double) try_bounds.y, + try_cleared?" (pixels cleared)":""); +#endif + } + /* + Test if this background dispose is smaller than any of the + other methods we tryed before this (including duplicated frame) + */ + if ( cleared || + bgnd_bounds.width*bgnd_bounds.height + +try_bounds.width*try_bounds.height + < bounds[i-1].width*bounds[i-1].height + +dup_bounds.width*dup_bounds.height + +bounds[i].width*bounds[i].height ) + { + cleared=MagickFalse; + bounds[i-1]=bgnd_bounds; + bounds[i]=try_bounds; + if ( disposals[i-1] == DupDispose ) + dup_image=DestroyImage(dup_image); + disposals[i-1]=BackgroundDispose; +#if DEBUG_OPT_FRAME + (void) FormatLocaleFile(stderr,"expand_bgnd: accepted\n"); + } else { + (void) FormatLocaleFile(stderr,"expand_bgnd: reject\n"); +#endif + } + } + /* + Finalise choice of dispose, set new prev_image, + and junk any extra images as appropriate, + */ + if ( disposals[i-1] == DupDispose ) + { + if (bgnd_image != (Image *) NULL) + bgnd_image=DestroyImage(bgnd_image); + prev_image=DestroyImage(prev_image); + prev_image=dup_image, dup_image=(Image *) NULL; + bounds[i+1]=bounds[i]; + bounds[i]=dup_bounds; + disposals[i-1]=DupDispose; + disposals[i]=BackgroundDispose; + i++; + } + else + { + if ( dup_image != (Image *) NULL) + dup_image=DestroyImage(dup_image); + if ( disposals[i-1] != PreviousDispose ) + prev_image=DestroyImage(prev_image); + if ( disposals[i-1] == BackgroundDispose ) + prev_image=bgnd_image, bgnd_image=(Image *) NULL; + if (bgnd_image != (Image *) NULL) + bgnd_image=DestroyImage(bgnd_image); + if ( disposals[i-1] == NoneDispose ) + { + prev_image=ReferenceImage(curr->previous); + if (prev_image == (Image *) NULL) + { + bounds=(RectangleInfo *) RelinquishMagickMemory(bounds); + disposals=(DisposeType *) RelinquishMagickMemory(disposals); + return((Image *) NULL); + } + } + + } + assert(prev_image != (Image *) NULL); + disposals[i]=disposals[i-1]; +#if DEBUG_OPT_FRAME + (void) FormatLocaleFile(stderr, "final %.20g : %s %.20gx%.20g%+.20g%+.20g\n", + (double) i-1, + CommandOptionToMnemonic(MagickDisposeOptions,disposals[i-1]), + (double) bounds[i-1].width,(double) bounds[i-1].height, + (double) bounds[i-1].x,(double) bounds[i-1].y ); +#endif +#if DEBUG_OPT_FRAME + (void) FormatLocaleFile(stderr, "interum %.20g : %s %.20gx%.20g%+.20g%+.20g\n", + (double) i, + CommandOptionToMnemonic(MagickDisposeOptions,disposals[i]), + (double) bounds[i].width,(double) bounds[i].height, + (double) bounds[i].x,(double) bounds[i].y ); + (void) FormatLocaleFile(stderr,"\n"); +#endif + i++; + } + prev_image=DestroyImage(prev_image); + /* + Optimize all images in sequence. + */ + sans_exception=AcquireExceptionInfo(); + i=0; + curr=GetFirstImageInList(image); + optimized_image=NewImageList(); + while ( curr != (const Image *) NULL ) + { + prev_image=CloneImage(curr,0,0,MagickTrue,exception); + if (prev_image == (Image *) NULL) + break; + if (prev_image->alpha_trait == UndefinedPixelTrait) + (void) SetImageAlphaChannel(prev_image,OpaqueAlphaChannel,exception); + if ( disposals[i] == DelDispose ) { + size_t time = 0; + while ( disposals[i] == DelDispose ) { + time += curr->delay*1000/curr->ticks_per_second; + curr=GetNextImageInList(curr); + i++; + } + time += curr->delay*1000/curr->ticks_per_second; + prev_image->ticks_per_second = 100L; + prev_image->delay = time*prev_image->ticks_per_second/1000; + } + bgnd_image=CropImage(prev_image,&bounds[i],sans_exception); + prev_image=DestroyImage(prev_image); + if (bgnd_image == (Image *) NULL) + break; + bgnd_image->dispose=disposals[i]; + if ( disposals[i] == DupDispose ) { + bgnd_image->delay=0; + bgnd_image->dispose=NoneDispose; + } + else + curr=GetNextImageInList(curr); + AppendImageToList(&optimized_image,bgnd_image); + i++; + } + sans_exception=DestroyExceptionInfo(sans_exception); + bounds=(RectangleInfo *) RelinquishMagickMemory(bounds); + disposals=(DisposeType *) RelinquishMagickMemory(disposals); + if (curr != (Image *) NULL) + { + optimized_image=DestroyImageList(optimized_image); + return((Image *) NULL); + } + return(GetFirstImageInList(optimized_image)); +} + +/* +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% % +% % +% % +% O p t i m i z e I m a g e L a y e r s % +% % +% % +% % +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% +% OptimizeImageLayers() compares each image the GIF disposed forms of the +% previous image in the sequence. From this it attempts to select the +% smallest cropped image to replace each frame, while preserving the results +% of the GIF animation. +% +% The format of the OptimizeImageLayers method is: +% +% Image *OptimizeImageLayers(const Image *image, +% ExceptionInfo *exception) +% +% A description of each parameter follows: +% +% o image: the image. +% +% o exception: return any errors or warnings in this structure. +% +*/ +MagickExport Image *OptimizeImageLayers(const Image *image, + ExceptionInfo *exception) +{ + return(OptimizeLayerFrames(image,OptimizeImageLayer,exception)); +} + +/* +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% % +% % +% % +% O p t i m i z e P l u s I m a g e L a y e r s % +% % +% % +% % +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% +% OptimizeImagePlusLayers() is exactly as OptimizeImageLayers(), but may +% also add or even remove extra frames in the animation, if it improves +% the total number of pixels in the resulting GIF animation. +% +% The format of the OptimizePlusImageLayers method is: +% +% Image *OptimizePlusImageLayers(const Image *image, +% ExceptionInfo *exception) +% +% A description of each parameter follows: +% +% o image: the image. +% +% o exception: return any errors or warnings in this structure. +% +*/ +MagickExport Image *OptimizePlusImageLayers(const Image *image, + ExceptionInfo *exception) +{ + return OptimizeLayerFrames(image,OptimizePlusLayer,exception); +} + +/* +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% % +% % +% % +% O p t i m i z e I m a g e T r a n s p a r e n c y % +% % +% % +% % +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% +% OptimizeImageTransparency() takes a frame optimized GIF animation, and +% compares the overlayed pixels against the disposal image resulting from all +% the previous frames in the animation. Any pixel that does not change the +% disposal image (and thus does not effect the outcome of an overlay) is made +% transparent. +% +% WARNING: This modifies the current images directly, rather than generate +% a new image sequence. +% +% The format of the OptimizeImageTransperency method is: +% +% void OptimizeImageTransperency(Image *image,ExceptionInfo *exception) +% +% A description of each parameter follows: +% +% o image: the image sequence +% +% o exception: return any errors or warnings in this structure. +% +*/ +MagickExport void OptimizeImageTransparency(const Image *image, + ExceptionInfo *exception) +{ + Image + *dispose_image; + + register Image + *next; + + /* + Run the image through the animation sequence + */ + assert(image != (Image *) NULL); + assert(image->signature == MagickCoreSignature); + if (image->debug != MagickFalse) + (void) LogMagickEvent(TraceEvent,GetMagickModule(),"%s",image->filename); + assert(exception != (ExceptionInfo *) NULL); + assert(exception->signature == MagickCoreSignature); + next=GetFirstImageInList(image); + dispose_image=CloneImage(next,next->page.width,next->page.height, + MagickTrue,exception); + if (dispose_image == (Image *) NULL) + return; + dispose_image->page=next->page; + dispose_image->page.x=0; + dispose_image->page.y=0; + dispose_image->dispose=NoneDispose; + dispose_image->background_color.alpha_trait=BlendPixelTrait; + dispose_image->background_color.alpha=(MagickRealType) TransparentAlpha; + (void) SetImageBackgroundColor(dispose_image,exception); + + while ( next != (Image *) NULL ) + { + Image + *current_image; + + /* + Overlay this frame's image over the previous disposal image + */ + current_image=CloneImage(dispose_image,0,0,MagickTrue,exception); + if (current_image == (Image *) NULL) + { + dispose_image=DestroyImage(dispose_image); + return; + } + (void) CompositeImage(current_image,next,next->alpha_trait != UndefinedPixelTrait ? + OverCompositeOp : CopyCompositeOp,MagickTrue,next->page.x,next->page.y, + exception); + /* + At this point the image would be displayed, for the delay period + ** + Work out the disposal of the previous image + */ + if (next->dispose == BackgroundDispose) + { + RectangleInfo + bounds=next->page; + + bounds.width=next->columns; + bounds.height=next->rows; + if (bounds.x < 0) + { + bounds.width+=bounds.x; + bounds.x=0; + } + if ((ssize_t) (bounds.x+bounds.width) > (ssize_t) current_image->columns) + bounds.width=current_image->columns-bounds.x; + if (bounds.y < 0) + { + bounds.height+=bounds.y; + bounds.y=0; + } + if ((ssize_t) (bounds.y+bounds.height) > (ssize_t) current_image->rows) + bounds.height=current_image->rows-bounds.y; + ClearBounds(current_image,&bounds,exception); + } + if (next->dispose != PreviousDispose) + { + dispose_image=DestroyImage(dispose_image); + dispose_image=current_image; + } + else + current_image=DestroyImage(current_image); + + /* + Optimize Transparency of the next frame (if present) + */ + next=GetNextImageInList(next); + if (next != (Image *) NULL) { + (void) CompositeImage(next,dispose_image,ChangeMaskCompositeOp, + MagickTrue,-(next->page.x),-(next->page.y),exception); + } + } + dispose_image=DestroyImage(dispose_image); + return; +} + +/* +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% % +% % +% % +% R e m o v e D u p l i c a t e L a y e r s % +% % +% % +% % +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% +% RemoveDuplicateLayers() removes any image that is exactly the same as the +% next image in the given image list. Image size and virtual canvas offset +% must also match, though not the virtual canvas size itself. +% +% No check is made with regards to image disposal setting, though it is the +% dispose setting of later image that is kept. Also any time delays are also +% added together. As such coalesced image animations should still produce the +% same result, though with duplicte frames merged into a single frame. +% +% The format of the RemoveDuplicateLayers method is: +% +% void RemoveDuplicateLayers(Image **image,ExceptionInfo *exception) +% +% A description of each parameter follows: +% +% o images: the image list +% +% o exception: return any errors or warnings in this structure. +% +*/ +MagickExport void RemoveDuplicateLayers(Image **images, + ExceptionInfo *exception) +{ + register Image + *curr, + *next; + + RectangleInfo + bounds; + + assert((*images) != (const Image *) NULL); + assert((*images)->signature == MagickCoreSignature); + if ((*images)->debug != MagickFalse) + (void) LogMagickEvent(TraceEvent,GetMagickModule(),"%s",(*images)->filename); + assert(exception != (ExceptionInfo *) NULL); + assert(exception->signature == MagickCoreSignature); + + curr=GetFirstImageInList(*images); + for (; (next=GetNextImageInList(curr)) != (Image *) NULL; curr=next) + { + if ( curr->columns != next->columns || curr->rows != next->rows + || curr->page.x != next->page.x || curr->page.y != next->page.y ) + continue; + bounds=CompareImagesBounds(curr,next,CompareAnyLayer,exception); + if ( bounds.x < 0 ) { + /* + the two images are the same, merge time delays and delete one. + */ + size_t time; + time = curr->delay*1000/curr->ticks_per_second; + time += next->delay*1000/next->ticks_per_second; + next->ticks_per_second = 100L; + next->delay = time*curr->ticks_per_second/1000; + next->iterations = curr->iterations; + *images = curr; + (void) DeleteImageFromList(images); + } + } + *images = GetFirstImageInList(*images); +} + +/* +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% % +% % +% % +% R e m o v e Z e r o D e l a y L a y e r s % +% % +% % +% % +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% +% RemoveZeroDelayLayers() removes any image that as a zero delay time. Such +% images generally represent intermediate or partial updates in GIF +% animations used for file optimization. They are not ment to be displayed +% to users of the animation. Viewable images in an animation should have a +% time delay of 3 or more centi-seconds (hundredths of a second). +% +% However if all the frames have a zero time delay, then either the animation +% is as yet incomplete, or it is not a GIF animation. This a non-sensible +% situation, so no image will be removed and a 'Zero Time Animation' warning +% (exception) given. +% +% No warning will be given if no image was removed because all images had an +% appropriate non-zero time delay set. +% +% Due to the special requirements of GIF disposal handling, GIF animations +% should be coalesced first, before calling this function, though that is not +% a requirement. +% +% The format of the RemoveZeroDelayLayers method is: +% +% void RemoveZeroDelayLayers(Image **image,ExceptionInfo *exception) +% +% A description of each parameter follows: +% +% o images: the image list +% +% o exception: return any errors or warnings in this structure. +% +*/ +MagickExport void RemoveZeroDelayLayers(Image **images, + ExceptionInfo *exception) +{ + Image + *i; + + assert((*images) != (const Image *) NULL); + assert((*images)->signature == MagickCoreSignature); + if ((*images)->debug != MagickFalse) + (void) LogMagickEvent(TraceEvent,GetMagickModule(),"%s",(*images)->filename); + assert(exception != (ExceptionInfo *) NULL); + assert(exception->signature == MagickCoreSignature); + + i=GetFirstImageInList(*images); + for ( ; i != (Image *) NULL; i=GetNextImageInList(i)) + if ( i->delay != 0L ) break; + if ( i == (Image *) NULL ) { + (void) ThrowMagickException(exception,GetMagickModule(),OptionWarning, + "ZeroTimeAnimation","`%s'",GetFirstImageInList(*images)->filename); + return; + } + i=GetFirstImageInList(*images); + while ( i != (Image *) NULL ) + { + if ( i->delay == 0L ) { + (void) DeleteImageFromList(&i); + *images=i; + } + else + i=GetNextImageInList(i); + } + *images=GetFirstImageInList(*images); +} + +/* +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% % +% % +% % +% C o m p o s i t e L a y e r s % +% % +% % +% % +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% +% CompositeLayers() compose the source image sequence over the destination +% image sequence, starting with the current image in both lists. +% +% Each layer from the two image lists are composted together until the end of +% one of the image lists is reached. The offset of each composition is also +% adjusted to match the virtual canvas offsets of each layer. As such the +% given offset is relative to the virtual canvas, and not the actual image. +% +% Composition uses given x and y offsets, as the 'origin' location of the +% source images virtual canvas (not the real image) allowing you to compose a +% list of 'layer images' into the destiantioni images. This makes it well +% sutiable for directly composing 'Clears Frame Animations' or 'Coaleased +% Animations' onto a static or other 'Coaleased Animation' destination image +% list. GIF disposal handling is not looked at. +% +% Special case:- If one of the image sequences is the last image (just a +% single image remaining), that image is repeatally composed with all the +% images in the other image list. Either the source or destination lists may +% be the single image, for this situation. +% +% In the case of a single destination image (or last image given), that image +% will ve cloned to match the number of images remaining in the source image +% list. +% +% This is equivelent to the "-layer Composite" Shell API operator. +% +% +% The format of the CompositeLayers method is: +% +% void CompositeLayers(Image *destination, const CompositeOperator +% compose, Image *source, const ssize_t x_offset, const ssize_t y_offset, +% ExceptionInfo *exception); +% +% A description of each parameter follows: +% +% o destination: the destination images and results +% +% o source: source image(s) for the layer composition +% +% o compose, x_offset, y_offset: arguments passed on to CompositeImages() +% +% o exception: return any errors or warnings in this structure. +% +*/ + +static inline void CompositeCanvas(Image *destination, + const CompositeOperator compose,Image *source,ssize_t x_offset, + ssize_t y_offset,ExceptionInfo *exception) +{ + const char + *value; + + x_offset+=source->page.x-destination->page.x; + y_offset+=source->page.y-destination->page.y; + value=GetImageArtifact(source,"compose:outside-overlay"); + (void) CompositeImage(destination,source,compose, + (value != (const char *) NULL) && (IsStringTrue(value) != MagickFalse) ? + MagickFalse : MagickTrue,x_offset,y_offset,exception); +} + +MagickExport void CompositeLayers(Image *destination, + const CompositeOperator compose, Image *source,const ssize_t x_offset, + const ssize_t y_offset,ExceptionInfo *exception) +{ + assert(destination != (Image *) NULL); + assert(destination->signature == MagickCoreSignature); + assert(source != (Image *) NULL); + assert(source->signature == MagickCoreSignature); + assert(exception != (ExceptionInfo *) NULL); + assert(exception->signature == MagickCoreSignature); + if (source->debug != MagickFalse || destination->debug != MagickFalse) + (void) LogMagickEvent(TraceEvent,GetMagickModule(),"%s - %s", + source->filename,destination->filename); + + /* + Overlay single source image over destation image/list + */ + if ( source->next == (Image *) NULL ) + while ( destination != (Image *) NULL ) + { + CompositeCanvas(destination, compose, source, x_offset, y_offset, + exception); + destination=GetNextImageInList(destination); + } + + /* + Overlay source image list over single destination. + Multiple clones of destination image are created to match source list. + Original Destination image becomes first image of generated list. + As such the image list pointer does not require any change in caller. + Some animation attributes however also needs coping in this case. + */ + else if ( destination->next == (Image *) NULL ) + { + Image *dest = CloneImage(destination,0,0,MagickTrue,exception); + + CompositeCanvas(destination, compose, source, x_offset, y_offset, + exception); + /* copy source image attributes ? */ + if ( source->next != (Image *) NULL ) + { + destination->delay = source->delay; + destination->iterations = source->iterations; + } + source=GetNextImageInList(source); + + while ( source != (Image *) NULL ) + { + AppendImageToList(&destination, + CloneImage(dest,0,0,MagickTrue,exception)); + destination=GetLastImageInList(destination); + + CompositeCanvas(destination, compose, source, x_offset, y_offset, + exception); + destination->delay = source->delay; + destination->iterations = source->iterations; + source=GetNextImageInList(source); + } + dest=DestroyImage(dest); + } + + /* + Overlay a source image list over a destination image list + until either list runs out of images. (Does not repeat) + */ + else + while ( source != (Image *) NULL && destination != (Image *) NULL ) + { + CompositeCanvas(destination, compose, source, x_offset, y_offset, + exception); + source=GetNextImageInList(source); + destination=GetNextImageInList(destination); + } +} + +/* +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% % +% % +% % +% M e r g e I m a g e L a y e r s % +% % +% % +% % +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% +% MergeImageLayers() composes all the image layers from the current given +% image onward to produce a single image of the merged layers. +% +% The inital canvas's size depends on the given LayerMethod, and is +% initialized using the first images background color. The images +% are then compositied onto that image in sequence using the given +% composition that has been assigned to each individual image. +% +% The format of the MergeImageLayers is: +% +% Image *MergeImageLayers(Image *image,const LayerMethod method, +% ExceptionInfo *exception) +% +% A description of each parameter follows: +% +% o image: the image list to be composited together +% +% o method: the method of selecting the size of the initial canvas. +% +% MergeLayer: Merge all layers onto a canvas just large enough +% to hold all the actual images. The virtual canvas of the +% first image is preserved but otherwise ignored. +% +% FlattenLayer: Use the virtual canvas size of first image. +% Images which fall outside this canvas is clipped. +% This can be used to 'fill out' a given virtual canvas. +% +% MosaicLayer: Start with the virtual canvas of the first image, +% enlarging left and right edges to contain all images. +% Images with negative offsets will be clipped. +% +% TrimBoundsLayer: Determine the overall bounds of all the image +% layers just as in "MergeLayer", then adjust the the canvas +% and offsets to be relative to those bounds, without overlaying +% the images. +% +% WARNING: a new image is not returned, the original image +% sequence page data is modified instead. +% +% o exception: return any errors or warnings in this structure. +% +*/ +MagickExport Image *MergeImageLayers(Image *image,const LayerMethod method, + ExceptionInfo *exception) +{ +#define MergeLayersTag "Merge/Layers" + + Image + *canvas; + + MagickBooleanType + proceed; + + RectangleInfo + page; + + register const Image + *next; + + size_t + number_images, + height, + width; + + ssize_t + scene; + + assert(image != (Image *) NULL); + assert(image->signature == MagickCoreSignature); + if (image->debug != MagickFalse) + (void) LogMagickEvent(TraceEvent,GetMagickModule(),"%s",image->filename); + assert(exception != (ExceptionInfo *) NULL); + assert(exception->signature == MagickCoreSignature); + /* + Determine canvas image size, and its virtual canvas size and offset + */ + page=image->page; + width=image->columns; + height=image->rows; + switch (method) + { + case TrimBoundsLayer: + case MergeLayer: + default: + { + next=GetNextImageInList(image); + for ( ; next != (Image *) NULL; next=GetNextImageInList(next)) + { + if (page.x > next->page.x) + { + width+=page.x-next->page.x; + page.x=next->page.x; + } + if (page.y > next->page.y) + { + height+=page.y-next->page.y; + page.y=next->page.y; + } + if ((ssize_t) width < (next->page.x+(ssize_t) next->columns-page.x)) + width=(size_t) next->page.x+(ssize_t) next->columns-page.x; + if ((ssize_t) height < (next->page.y+(ssize_t) next->rows-page.y)) + height=(size_t) next->page.y+(ssize_t) next->rows-page.y; + } + break; + } + case FlattenLayer: + { + if (page.width > 0) + width=page.width; + if (page.height > 0) + height=page.height; + page.x=0; + page.y=0; + break; + } + case MosaicLayer: + { + if (page.width > 0) + width=page.width; + if (page.height > 0) + height=page.height; + for (next=image; next != (Image *) NULL; next=GetNextImageInList(next)) + { + if (method == MosaicLayer) + { + page.x=next->page.x; + page.y=next->page.y; + if ((ssize_t) width < (next->page.x+(ssize_t) next->columns)) + width=(size_t) next->page.x+next->columns; + if ((ssize_t) height < (next->page.y+(ssize_t) next->rows)) + height=(size_t) next->page.y+next->rows; + } + } + page.width=width; + page.height=height; + page.x=0; + page.y=0; + } + break; + } + /* + Set virtual canvas size if not defined. + */ + if (page.width == 0) + page.width=page.x < 0 ? width : width+page.x; + if (page.height == 0) + page.height=page.y < 0 ? height : height+page.y; + /* + Handle "TrimBoundsLayer" method separately to normal 'layer merge'. + */ + if (method == TrimBoundsLayer) + { + number_images=GetImageListLength(image); + for (scene=0; scene < (ssize_t) number_images; scene++) + { + image->page.x-=page.x; + image->page.y-=page.y; + image->page.width=width; + image->page.height=height; + proceed=SetImageProgress(image,MergeLayersTag,(MagickOffsetType) scene, + number_images); + if (proceed == MagickFalse) + break; + image=GetNextImageInList(image); + if (image == (Image *) NULL) + break; + } + return((Image *) NULL); + } + /* + Create canvas size of width and height, and background color. + */ + canvas=CloneImage(image,width,height,MagickTrue,exception); + if (canvas == (Image *) NULL) + return((Image *) NULL); + (void) SetImageBackgroundColor(canvas,exception); + canvas->page=page; + canvas->dispose=UndefinedDispose; + /* + Compose images onto canvas, with progress monitor + */ + number_images=GetImageListLength(image); + for (scene=0; scene < (ssize_t) number_images; scene++) + { + (void) CompositeImage(canvas,image,image->compose,MagickTrue,image->page.x- + canvas->page.x,image->page.y-canvas->page.y,exception); + proceed=SetImageProgress(image,MergeLayersTag,(MagickOffsetType) scene, + number_images); + if (proceed == MagickFalse) + break; + image=GetNextImageInList(image); + if (image == (Image *) NULL) + break; + } + return(canvas); +} +