blob: 56b0c9cb5ac6b099bb9c3f7fc9ad5fc7a3b1d7a4 [file] [log] [blame]
/*
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% %
% %
% 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);
}