forked from openkylin/imagemagick
2099 lines
75 KiB
C
2099 lines
75 KiB
C
/*
|
||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||
% %
|
||
% %
|
||
% 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 "magick/studio.h"
|
||
#include "magick/artifact.h"
|
||
#include "magick/cache.h"
|
||
#include "magick/channel.h"
|
||
#include "magick/color.h"
|
||
#include "magick/color-private.h"
|
||
#include "magick/composite.h"
|
||
#include "magick/effect.h"
|
||
#include "magick/exception.h"
|
||
#include "magick/exception-private.h"
|
||
#include "magick/geometry.h"
|
||
#include "magick/image.h"
|
||
#include "magick/layer.h"
|
||
#include "magick/list.h"
|
||
#include "magick/memory_.h"
|
||
#include "magick/monitor.h"
|
||
#include "magick/monitor-private.h"
|
||
#include "magick/option.h"
|
||
#include "magick/pixel-private.h"
|
||
#include "magick/property.h"
|
||
#include "magick/profile.h"
|
||
#include "magick/resource_.h"
|
||
#include "magick/resize.h"
|
||
#include "magick/statistic.h"
|
||
#include "magick/string_.h"
|
||
#include "magick/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)
|
||
%
|
||
% 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
|
||
%
|
||
*/
|
||
static void ClearBounds(Image *image,RectangleInfo *bounds)
|
||
{
|
||
ExceptionInfo
|
||
*exception;
|
||
|
||
ssize_t
|
||
y;
|
||
|
||
if (bounds->x < 0)
|
||
return;
|
||
if (image->matte == MagickFalse)
|
||
(void) SetImageAlphaChannel(image,OpaqueAlphaChannel);
|
||
exception=(&image->exception);
|
||
for (y=0; y < (ssize_t) bounds->height; y++)
|
||
{
|
||
ssize_t
|
||
x;
|
||
|
||
PixelPacket
|
||
*magick_restrict q;
|
||
|
||
q=GetAuthenticPixels(image,bounds->x,bounds->y+y,bounds->width,1,exception);
|
||
if (q == (PixelPacket *) NULL)
|
||
break;
|
||
for (x=0; x < (ssize_t) bounds->width; x++)
|
||
{
|
||
q->opacity=(Quantum) TransparentOpacity;
|
||
q++;
|
||
}
|
||
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 PixelPacket
|
||
*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 PixelPacket *) NULL) || (q == (PixelPacket *) NULL))
|
||
break;
|
||
for (x=0; x < (ssize_t) bounds->width; x++)
|
||
{
|
||
if ((GetPixelOpacity(p) <= (Quantum) (QuantumRange/2)) &&
|
||
(GetPixelOpacity(q) > (Quantum) (QuantumRange/2)))
|
||
break;
|
||
p++;
|
||
q++;
|
||
}
|
||
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);
|
||
|
||
/* initialise first image */
|
||
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.opacity=(Quantum) TransparentOpacity;
|
||
(void) SetImageBackgroundColor(coalesce_image);
|
||
coalesce_image->matte=next->matte;
|
||
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,CopyCompositeOp,next,next->page.x,
|
||
next->page.y);
|
||
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);
|
||
/*
|
||
Next image is the dispose image, overlaid with next frame in sequence.
|
||
*/
|
||
coalesce_image->next=CloneImage(dispose_image,0,0,MagickTrue,exception);
|
||
if (coalesce_image->next != NULL)
|
||
coalesce_image->next->previous=coalesce_image;
|
||
previous=coalesce_image;
|
||
coalesce_image=GetNextImageInList(coalesce_image);
|
||
(void) CompositeImage(coalesce_image,next->matte != MagickFalse ?
|
||
OverCompositeOp : CopyCompositeOp,next,next->page.x,next->page.y);
|
||
(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 *images,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 *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.opacity=(Quantum) TransparentOpacity;
|
||
(void) SetImageBackgroundColor(dispose_image);
|
||
dispose_images=NewImageList();
|
||
for (next=image; next != (Image *) NULL; next=GetNextImageInList(next))
|
||
{
|
||
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->matte != MagickFalse ?
|
||
OverCompositeOp : CopyCompositeOp,next,next->page.x,next->page.y);
|
||
/*
|
||
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);
|
||
}
|
||
/*
|
||
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 CompareImageBounds(). It is
|
||
% doubtful that this sub-routine will be useful outside this module.
|
||
%
|
||
% The format of the ComparePixels method is:
|
||
%
|
||
% MagickBooleanType *ComparePixels(const ImageLayerMethod method,
|
||
% const MagickPixelPacket *p,const MagickPixelPacket *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 ImageLayerMethod method,
|
||
const MagickPixelPacket *p,const MagickPixelPacket *q)
|
||
{
|
||
MagickRealType
|
||
o1,
|
||
o2;
|
||
|
||
/*
|
||
Any change in pixel values
|
||
*/
|
||
if (method == CompareAnyLayer)
|
||
return((MagickBooleanType)(IsMagickColorSimilar(p,q) == MagickFalse));
|
||
|
||
o1 = (p->matte != MagickFalse) ? GetPixelOpacity(p) : OpaqueOpacity;
|
||
o2 = (q->matte != MagickFalse) ? q->opacity : OpaqueOpacity;
|
||
|
||
/*
|
||
Pixel goes from opaque to transprency
|
||
*/
|
||
if (method == CompareClearLayer)
|
||
return((MagickBooleanType) ( (o1 <= ((MagickRealType) QuantumRange/2.0)) &&
|
||
(o2 > ((MagickRealType) QuantumRange/2.0)) ) );
|
||
|
||
/*
|
||
overlay would change first pixel by second
|
||
*/
|
||
if (method == CompareOverlayLayer)
|
||
{
|
||
if (o2 > ((MagickRealType) QuantumRange/2.0))
|
||
return MagickFalse;
|
||
return((MagickBooleanType) (IsMagickColorSimilar(p,q) == MagickFalse));
|
||
}
|
||
return(MagickFalse);
|
||
}
|
||
|
||
|
||
/*
|
||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||
% %
|
||
% %
|
||
% %
|
||
+ C o m p a r e I m a g e B o u n d s %
|
||
% %
|
||
% %
|
||
% %
|
||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||
%
|
||
% CompareImageBounds() 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 CompareImageBounds method is:
|
||
%
|
||
% RectangleInfo *CompareImageBounds(const ImageLayerMethod 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 CompareImageBounds(const Image *image1,const Image *image2,
|
||
const ImageLayerMethod method,ExceptionInfo *exception)
|
||
{
|
||
RectangleInfo
|
||
bounds;
|
||
|
||
MagickPixelPacket
|
||
pixel1,
|
||
pixel2;
|
||
|
||
const IndexPacket
|
||
*indexes1,
|
||
*indexes2;
|
||
|
||
const PixelPacket
|
||
*p,
|
||
*q;
|
||
|
||
ssize_t
|
||
x;
|
||
|
||
ssize_t
|
||
y;
|
||
|
||
#if 0
|
||
/* only same sized images can be compared */
|
||
assert(image1->columns == image2->columns);
|
||
assert(image1->rows == image2->rows);
|
||
#endif
|
||
|
||
/*
|
||
Set bounding box of the differences between images
|
||
*/
|
||
GetMagickPixelPacket(image1,&pixel1);
|
||
GetMagickPixelPacket(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 PixelPacket *) NULL) || (q == (const PixelPacket *) NULL))
|
||
break;
|
||
indexes1=GetVirtualIndexQueue(image1);
|
||
indexes2=GetVirtualIndexQueue(image2);
|
||
for (y=0; y < (ssize_t) image1->rows; y++)
|
||
{
|
||
SetMagickPixelPacket(image1,p,indexes1+x,&pixel1);
|
||
SetMagickPixelPacket(image2,q,indexes2+x,&pixel2);
|
||
if (ComparePixels(method,&pixel1,&pixel2))
|
||
break;
|
||
p++;
|
||
q++;
|
||
}
|
||
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 PixelPacket *) NULL) || (q == (const PixelPacket *) NULL))
|
||
break;
|
||
indexes1=GetVirtualIndexQueue(image1);
|
||
indexes2=GetVirtualIndexQueue(image2);
|
||
for (y=0; y < (ssize_t) image1->rows; y++)
|
||
{
|
||
SetMagickPixelPacket(image1,p,indexes1+x,&pixel1);
|
||
SetMagickPixelPacket(image2,q,indexes2+x,&pixel2);
|
||
if (ComparePixels(method,&pixel1,&pixel2))
|
||
break;
|
||
p++;
|
||
q++;
|
||
}
|
||
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 PixelPacket *) NULL) || (q == (const PixelPacket *) NULL))
|
||
break;
|
||
indexes1=GetVirtualIndexQueue(image1);
|
||
indexes2=GetVirtualIndexQueue(image2);
|
||
for (x=0; x < (ssize_t) image1->columns; x++)
|
||
{
|
||
SetMagickPixelPacket(image1,p,indexes1+x,&pixel1);
|
||
SetMagickPixelPacket(image2,q,indexes2+x,&pixel2);
|
||
if (ComparePixels(method,&pixel1,&pixel2))
|
||
break;
|
||
p++;
|
||
q++;
|
||
}
|
||
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 PixelPacket *) NULL) || (q == (const PixelPacket *) NULL))
|
||
break;
|
||
indexes1=GetVirtualIndexQueue(image1);
|
||
indexes2=GetVirtualIndexQueue(image2);
|
||
for (x=0; x < (ssize_t) image1->columns; x++)
|
||
{
|
||
SetMagickPixelPacket(image1,p,indexes1+x,&pixel1);
|
||
SetMagickPixelPacket(image2,q,indexes2+x,&pixel2);
|
||
if (ComparePixels(method,&pixel1,&pixel2))
|
||
break;
|
||
p++;
|
||
q++;
|
||
}
|
||
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 %
|
||
% %
|
||
% %
|
||
% %
|
||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||
%
|
||
% CompareImageLayers() compares each image with the next in a sequence and
|
||
% returns the minimum bounding region of all the pixel differences (of the
|
||
% ImageLayerMethod 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 CompareImageLayers method is:
|
||
%
|
||
% Image *CompareImageLayers(const Image *images,
|
||
% const ImageLayerMethod 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 *CompareImageLayers(const Image *image,
|
||
const ImageLayerMethod 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.opacity=(Quantum) TransparentOpacity;
|
||
(void) SetImageBackgroundColor(image_a);
|
||
image_a->page=next->page;
|
||
image_a->page.x=0;
|
||
image_a->page.y=0;
|
||
(void) CompositeImage(image_a,CopyCompositeOp,next,next->page.x,next->page.y);
|
||
/*
|
||
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,CopyCompositeOp,next,next->page.x,
|
||
next->page.y);
|
||
bounds[i]=CompareImageBounds(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));
|
||
}
|
||
|
||
/*
|
||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||
% %
|
||
% %
|
||
% %
|
||
% D e c o n s t r u c t I m a g e s %
|
||
% %
|
||
% %
|
||
% %
|
||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||
%
|
||
% DeconstructImages() compares each image with the next in a sequence and
|
||
% returns the minimum bounding region of all differences from the first image.
|
||
%
|
||
% This function is deprecated in favor of the more universal
|
||
% CompareImageLayers() function.
|
||
%
|
||
% The format of the DeconstructImages method is:
|
||
%
|
||
% Image *DeconstructImages(const Image *images,ExceptionInfo *exception)
|
||
%
|
||
% A description of each parameter follows:
|
||
%
|
||
% o image: the image.
|
||
%
|
||
% o exception: return any errors or warnings in this structure.
|
||
%
|
||
*/
|
||
|
||
MagickExport Image *DeconstructImages(const Image *images,
|
||
ExceptionInfo *exception)
|
||
{
|
||
return(CompareImageLayers(images,CompareAnyLayer,exception));
|
||
}
|
||
|
||
/*
|
||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||
% %
|
||
% %
|
||
% %
|
||
+ 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:
|
||
%
|
||
% static Image *OptimizeLayerFrames(const Image *image,
|
||
% const ImageLayerMethod 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 ImageLayerMethod 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->page.width,curr->page.height,
|
||
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.opacity=(Quantum) TransparentOpacity;
|
||
(void) SetImageBackgroundColor(prev_image);
|
||
/*
|
||
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]=CompareImageBounds(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]=CompareImageBounds(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=CompareImageBounds(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=CompareImageBounds(dup_image,curr,CompareClearLayer,exception);
|
||
ClearBounds(dup_image,&dup_bounds);
|
||
try_bounds=CompareImageBounds(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);
|
||
try_bounds=CompareImageBounds(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=CompareImageBounds(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);
|
||
#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=CompareImageBounds(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=CompareImageBounds(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=CompareImageBounds(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 ( 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.opacity=(Quantum) TransparentOpacity;
|
||
(void) SetImageBackgroundColor(dispose_image);
|
||
|
||
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->matte != MagickFalse ?
|
||
OverCompositeOp : CopyCompositeOp,next,next->page.x,next->page.y);
|
||
/*
|
||
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);
|
||
}
|
||
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,ChangeMaskCompositeOp,dispose_image,
|
||
-(next->page.x),-(next->page.y));
|
||
}
|
||
}
|
||
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=CompareImageBounds(image,next,CompareAnyLayer,exception);
|
||
if (bounds.x < 0)
|
||
{
|
||
/*
|
||
Two images are the same, merge time delays and delete one.
|
||
*/
|
||
size_t
|
||
time;
|
||
|
||
time=1000*image->delay*PerceptibleReciprocal(image->ticks_per_second);
|
||
time+=1000*next->delay*PerceptibleReciprocal(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 )
|
||
{
|
||
x_offset+=source->page.x-destination->page.x;
|
||
y_offset+=source->page.y-destination->page.y;
|
||
(void) CompositeImage(destination,compose,source,x_offset,y_offset);
|
||
}
|
||
|
||
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);
|
||
destination=GetNextImageInList(destination);
|
||
}
|
||
|
||
/*
|
||
Overlay source image list over single destination
|
||
Generating multiple clones of destination image 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);
|
||
/* 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);
|
||
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);
|
||
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 ImageLayerMethod, 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(const Image *image,
|
||
% const ImageLayerMethod 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 ImageLayerMethod 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 ((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);
|
||
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->compose,image,image->page.x-
|
||
canvas->page.x,image->page.y-canvas->page.y);
|
||
proceed=SetImageProgress(image,MergeLayersTag,(MagickOffsetType) scene,
|
||
number_images);
|
||
if (proceed == MagickFalse)
|
||
break;
|
||
image=GetNextImageInList(image);
|
||
if (image == (Image *) NULL)
|
||
break;
|
||
}
|
||
return(canvas);
|
||
}
|