|
|
/*
|
|
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
|
|
% %
|
|
|
% %
|
|
|
% 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-2021 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/attribute.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++)
|
|
|
{
|
|
|
ssize_t
|
|
|
x;
|
|
|
|
|
|
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)
|
|
|
{
|
|
|
const Quantum
|
|
|
*p,
|
|
|
*q;
|
|
|
|
|
|
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;
|
|
|
|
|
|
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_trait=BlendPixelTrait;
|
|
|
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);
|
|
|
if (dispose_image == (Image *) NULL)
|
|
|
{
|
|
|
coalesce_image=DestroyImage(coalesce_image);
|
|
|
return((Image *) NULL);
|
|
|
}
|
|
|
dispose_image->background_color.alpha_trait=BlendPixelTrait;
|
|
|
(void) CompositeImage(coalesce_image,next,CopyCompositeOp,MagickTrue,
|
|
|
next->page.x,next->page.y,exception);
|
|
|
next=GetNextImageInList(next);
|
|
|
for ( ; next != (Image *) NULL; next=GetNextImageInList(next))
|
|
|
{
|
|
|
const char
|
|
|
*attribute;
|
|
|
|
|
|
/*
|
|
|
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);
|
|
|
}
|
|
|
dispose_image->background_color.alpha_trait=BlendPixelTrait;
|
|
|
}
|
|
|
/*
|
|
|
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);
|
|
|
coalesce_image->background_color.alpha_trait=BlendPixelTrait;
|
|
|
attribute=GetImageProperty(next,"webp:mux-blend",exception);
|
|
|
if (attribute == (const char *) NULL)
|
|
|
(void) CompositeImage(coalesce_image,next,
|
|
|
next->alpha_trait != UndefinedPixelTrait ? OverCompositeOp :
|
|
|
CopyCompositeOp,MagickTrue,next->page.x,next->page.y,exception);
|
|
|
else
|
|
|
(void) CompositeImage(coalesce_image,next,
|
|
|
LocaleCompare(attribute,"AtopBackgroundAlphaBlend") == 0 ?
|
|
|
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;
|
|
|
|
|
|
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_trait=BlendPixelTrait;
|
|
|
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);
|
|
|
}
|
|
|
current_image->background_color.alpha_trait=BlendPixelTrait;
|
|
|
(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);
|
|
|
}
|
|
|
dispose_image->background_color.alpha_trait=BlendPixelTrait;
|
|
|
(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(IsFuzzyEquivalencePixelInfo(p,q) == MagickFalse ? MagickTrue : 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(IsFuzzyEquivalencePixelInfo(p,q) == MagickFalse ? MagickTrue :
|
|
|
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;
|
|
|
|
|
|
const Quantum
|
|
|
*p,
|
|
|
*q;
|
|
|
|
|
|
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) != MagickFalse)
|
|
|
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) != MagickFalse)
|
|
|
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) != MagickFalse)
|
|
|
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) != MagickFalse)
|
|
|
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;
|
|
|
|
|
|
const Image
|
|
|
*next;
|
|
|
|
|
|
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_trait=BlendPixelTrait;
|
|
|
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);
|
|
|
}
|
|
|
image_b->background_color.alpha_trait=BlendPixelTrait;
|
|
|
(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);
|
|
|
}
|
|
|
layers->background_color.alpha_trait=BlendPixelTrait;
|
|
|
/*
|
|
|
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_a->background_color.alpha_trait=BlendPixelTrait;
|
|
|
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;
|
|
|
|
|
|
const Image
|
|
|
*curr;
|
|
|
|
|
|
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_image->background_color.alpha_trait=BlendPixelTrait;
|
|
|
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_image->background_color.alpha_trait=BlendPixelTrait;
|
|
|
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;
|
|
|
prev_image->background_color.alpha_trait=BlendPixelTrait;
|
|
|
if ( disposals[i] == DelDispose ) {
|
|
|
size_t time = 0;
|
|
|
while ( disposals[i] == DelDispose ) {
|
|
|
time +=(size_t) (curr->delay*1000*
|
|
|
PerceptibleReciprocal((double) curr->ticks_per_second));
|
|
|
curr=GetNextImageInList(curr);
|
|
|
i++;
|
|
|
}
|
|
|
time += (size_t)(curr->delay*1000*
|
|
|
PerceptibleReciprocal((double) 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;
|
|
|
|
|
|
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;
|
|
|
}
|
|
|
current_image->background_color.alpha_trait=BlendPixelTrait;
|
|
|
(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)
|
|
|
{
|
|
|
RectangleInfo
|
|
|
bounds;
|
|
|
|
|
|
Image
|
|
|
*image,
|
|
|
*next;
|
|
|
|
|
|
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);
|
|
|
image=GetFirstImageInList(*images);
|
|
|
for ( ; (next=GetNextImageInList(image)) != (Image *) NULL; image=next)
|
|
|
{
|
|
|
if ((image->columns != next->columns) || (image->rows != next->rows) ||
|
|
|
(image->page.x != next->page.x) || (image->page.y != next->page.y))
|
|
|
continue;
|
|
|
bounds=CompareImagesBounds(image,next,CompareAnyLayer,exception);
|
|
|
if (bounds.x < 0)
|
|
|
{
|
|
|
/*
|
|
|
Two images are the same, merge time delays and delete one.
|
|
|
*/
|
|
|
size_t
|
|
|
time;
|
|
|
|
|
|
time=(size_t) (1000.0*image->delay*
|
|
|
PerceptibleReciprocal((double) image->ticks_per_second));
|
|
|
time+=(size_t) (1000.0*next->delay*
|
|
|
PerceptibleReciprocal((double) next->ticks_per_second));
|
|
|
next->ticks_per_second=100L;
|
|
|
next->delay=time*image->ticks_per_second/1000;
|
|
|
next->iterations=image->iterations;
|
|
|
*images=image;
|
|
|
(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);
|
|
|
|
|
|
if (dest != (Image *) NULL)
|
|
|
{
|
|
|
dest->background_color.alpha_trait=BlendPixelTrait;
|
|
|
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->background_color.alpha_trait=BlendPixelTrait;
|
|
|
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 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;
|
|
|
|
|
|
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);
|
|
|
canvas->background_color.alpha_trait=BlendPixelTrait;
|
|
|
(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);
|
|
|
}
|
|
|
|