/**
 * Image class methods.
 *
 * Copyright (c) 2002 - 2009 Timothy P. Hunter
 * Copyright (c) 2009 -      Benjamin Thomas and Omer Bar-or
 * Copyright (c) 2009 -      RMagick contributors
 *
 * @file     rmimage.cpp
 * @author   Tim Hunter
 */

#include "rmagick.h"
#include <signal.h>

#define BEGIN_CHANNEL_MASK(image, channels) \
  { \
    ChannelType channel_mask = SetPixelChannelMask(image, (ChannelType)channels);

#define END_CHANNEL_MASK(image) \
    SetPixelChannelMask(image, channel_mask); \
  }

#define CHANGE_RESULT_CHANNEL_MASK(result) \
    if (result != (Image *)NULL) \
      SetPixelChannelMask(result, channel_mask);

#ifndef magick_module
    #define magick_module module
#endif

static VALUE cropper(int, int, VALUE *, VALUE);
static VALUE effect_image(VALUE, int, VALUE *, gvl_function_t);
static VALUE flipflop(int, VALUE, gvl_function_t);
static VALUE rd_image(VALUE, VALUE, gvl_function_t);
static VALUE rotate(int, int, VALUE *, VALUE);
static VALUE scale(int, int, VALUE *, VALUE, gvl_function_t);
static VALUE threshold_image(int, VALUE *, VALUE, gvl_function_t);
static VALUE xform_image(int, VALUE, VALUE, VALUE, VALUE, VALUE, gvl_function_t);
static VALUE array_from_images(Image *);
static VALUE file_arg_rescue(VALUE, VALUE ATTRIBUTE_UNUSED) ATTRIBUTE_NORETURN;
static size_t rm_image_memsize(const void *img);

const rb_data_type_t rm_image_data_type = {
    "Magick::Image",
    { NULL, rm_image_destroy, rm_image_memsize, },
    0, 0,
    RUBY_TYPED_FROZEN_SHAREABLE,
};

static const char *BlackPointCompensationKey = "PROFILE:black-point-compensation";


DEFINE_GVL_STUB4(AdaptiveBlurImage, const Image *, const double, const double, ExceptionInfo *);
DEFINE_GVL_STUB4(AdaptiveResizeImage, const Image *, const size_t, const size_t, ExceptionInfo *);
DEFINE_GVL_STUB4(AdaptiveSharpenImage, const Image *, const double, const double, ExceptionInfo *);
DEFINE_GVL_STUB5(AdaptiveThresholdImage, const Image *, const size_t, const size_t, const double, ExceptionInfo *);
DEFINE_GVL_STUB3(AffineTransformImage, const Image *, const AffineMatrix *, ExceptionInfo *);
DEFINE_GVL_STUB2(Base64Decode, const char *, size_t *);
DEFINE_GVL_STUB4(BlobToImage, const ImageInfo *, const void *, const size_t, ExceptionInfo *);
DEFINE_GVL_STUB3(BlueShiftImage, const Image *, const double, ExceptionInfo *);
DEFINE_GVL_STUB4(BlurImage, const Image *, const double, const double, ExceptionInfo *);
DEFINE_GVL_STUB4(CharcoalImage, const Image *, const double, const double, ExceptionInfo *);
DEFINE_GVL_STUB3(ChopImage, const Image *, const RectangleInfo *, ExceptionInfo *);
DEFINE_GVL_STUB3(ColorMatrixImage, const Image *, const KernelInfo *, ExceptionInfo *);
DEFINE_GVL_STUB3(CropImage, const Image *, const RectangleInfo *, ExceptionInfo *);
DEFINE_GVL_STUB3(DecipherImage, Image *, const char *, ExceptionInfo *);
DEFINE_GVL_STUB3(DeskewImage, const Image *, const double, ExceptionInfo *);
DEFINE_GVL_STUB2(DespeckleImage, const Image *, ExceptionInfo *);
DEFINE_GVL_STUB6(DistortImage, const Image *, DistortMethod, const size_t, const double *, MagickBooleanType, ExceptionInfo *);
DEFINE_GVL_STUB3(EdgeImage, const Image *, const double, ExceptionInfo *);
DEFINE_GVL_STUB4(EmbossImage, const Image *, const double, const double, ExceptionInfo *);
DEFINE_GVL_STUB3(EncipherImage, Image *, const char *, ExceptionInfo *);
DEFINE_GVL_STUB2(EnhanceImage, const Image *, ExceptionInfo *);
DEFINE_GVL_STUB3(ExcerptImage, const Image *, const RectangleInfo *, ExceptionInfo *);
DEFINE_GVL_STUB9(ExportImagePixels, const Image *, const ssize_t, const ssize_t, const size_t, const size_t, const char *, const StorageType, void *, ExceptionInfo *);
DEFINE_GVL_STUB3(ExtentImage, const Image *, const RectangleInfo *, ExceptionInfo *);
DEFINE_GVL_STUB2(FlipImage, const Image *, ExceptionInfo *);
DEFINE_GVL_STUB2(FlopImage, const Image *, ExceptionInfo *);
DEFINE_GVL_STUB4(GaussianBlurImage, const Image *, const double, const double, ExceptionInfo *);
DEFINE_GVL_STUB6(GetAuthenticPixels, Image *, const ssize_t, const ssize_t, const size_t, const size_t, ExceptionInfo *);
DEFINE_GVL_STUB2(GetImageDepth, const Image *, ExceptionInfo *);
DEFINE_GVL_STUB3(GetImageHistogram, const Image *, size_t *, ExceptionInfo *);
DEFINE_GVL_STUB3(GetNumberColors, const Image *, FILE *, ExceptionInfo *);
DEFINE_GVL_STUB6(GetVirtualPixels, const Image *, const ssize_t, const ssize_t, const size_t, const size_t, ExceptionInfo *);
DEFINE_GVL_STUB4(ImageToBlob, const ImageInfo *, Image *, size_t *, ExceptionInfo *);
DEFINE_GVL_STUB6(LiquidRescaleImage, const Image *, const size_t, const size_t, const double, const double, ExceptionInfo *);
DEFINE_GVL_STUB2(MagnifyImage, const Image *, ExceptionInfo *);
DEFINE_GVL_STUB2(MinifyImage, const Image *, ExceptionInfo *);
DEFINE_GVL_STUB5(MotionBlurImage, const Image *, const double, const double, const double, ExceptionInfo *);
DEFINE_GVL_STUB2(PingImage, const ImageInfo *, ExceptionInfo *);
DEFINE_GVL_STUB3(PreviewImage, const Image *, const PreviewType, ExceptionInfo *);
DEFINE_GVL_STUB2(ReadImage, const ImageInfo *, ExceptionInfo *);
DEFINE_GVL_STUB4(RollImage, const Image *, const ssize_t, const ssize_t, ExceptionInfo *);
DEFINE_GVL_STUB3(RotateImage, const Image *, const double, ExceptionInfo *);
DEFINE_GVL_STUB4(SampleImage, const Image *, const size_t, const size_t, ExceptionInfo *);
DEFINE_GVL_STUB4(ScaleImage, const Image *, const size_t, const size_t, ExceptionInfo *);
DEFINE_GVL_STUB3(SepiaToneImage, const Image *, const double, ExceptionInfo *);
DEFINE_GVL_STUB5(ShadeImage, const Image *, const MagickBooleanType, const double, const double,ExceptionInfo *);
DEFINE_GVL_STUB6(ShadowImage, const Image *, const double, const double, const ssize_t, const ssize_t,  ExceptionInfo *);
DEFINE_GVL_STUB4(SharpenImage, const Image *, const double, const double, ExceptionInfo *);
DEFINE_GVL_STUB3(ShaveImage, const Image *, const RectangleInfo *, ExceptionInfo *);
DEFINE_GVL_STUB4(ShearImage, const Image *, const double, const double, ExceptionInfo *);
DEFINE_GVL_STUB5(SketchImage, const Image *, const double, const double, const double, ExceptionInfo *);
DEFINE_GVL_STUB3(SpliceImage, const Image *, const RectangleInfo *, ExceptionInfo *);
DEFINE_GVL_STUB5(StatisticImage, const Image *, const StatisticType, const size_t, const size_t, ExceptionInfo *);
DEFINE_GVL_STUB3(SteganoImage, const Image *, const Image *, ExceptionInfo *);
DEFINE_GVL_STUB3(StereoImage, const Image *, const Image *, ExceptionInfo *);
DEFINE_GVL_STUB2(SyncAuthenticPixels, Image *, ExceptionInfo *);
DEFINE_GVL_STUB4(ThumbnailImage, const Image *, const size_t, const size_t, ExceptionInfo *);
DEFINE_GVL_STUB2(TransposeImage, const Image *, ExceptionInfo *);
DEFINE_GVL_STUB2(TransverseImage, const Image *, ExceptionInfo *);
DEFINE_GVL_STUB2(TrimImage, const Image *, ExceptionInfo *);
DEFINE_GVL_STUB2(UniqueImageColors, const Image *, ExceptionInfo *);
DEFINE_GVL_STUB6(UnsharpMaskImage, const Image *, const double, const double, const double, const double,  ExceptionInfo *);
DEFINE_GVL_STUB6(VignetteImage, const Image *, const double, const double, const ssize_t, const ssize_t, ExceptionInfo *);
#if defined(IMAGEMAGICK_7)
DEFINE_GVL_STUB4(AddNoiseImage, const Image *, const NoiseType, const double, ExceptionInfo *);
DEFINE_GVL_STUB2(AutoGammaImage, Image *, ExceptionInfo *);
DEFINE_GVL_STUB2(AutoLevelImage, Image *, ExceptionInfo *);
DEFINE_GVL_STUB3(BilevelImage, Image *, const double, ExceptionInfo *);
DEFINE_GVL_STUB3(BlackThresholdImage, Image *, const char *, ExceptionInfo *);
DEFINE_GVL_STUB4(BorderImage, const Image *, const RectangleInfo *, const CompositeOperator, ExceptionInfo *);
DEFINE_GVL_STUB4(ClutImage, Image *, const Image *, const PixelInterpolateMethod, ExceptionInfo *);
DEFINE_GVL_STUB4(ColorizeImage, const Image *, const char *, const PixelInfo *, ExceptionInfo *);
DEFINE_GVL_STUB5(CompareImages, Image *, const Image *, const MetricType, double *, ExceptionInfo *);
DEFINE_GVL_STUB7(CompositeImage, Image *, const Image *, const CompositeOperator, const MagickBooleanType, const ssize_t, const ssize_t, ExceptionInfo *);
DEFINE_GVL_STUB2(CompressImageColormap, Image *, ExceptionInfo *);
DEFINE_GVL_STUB3(ContrastImage, Image *, const MagickBooleanType, ExceptionInfo *);
DEFINE_GVL_STUB4(ContrastStretchImage, Image *, const double, const double, ExceptionInfo *);
DEFINE_GVL_STUB3(ConvolveImage, const Image *, const KernelInfo *, ExceptionInfo *);
DEFINE_GVL_STUB3(CycleColormapImage, Image *, const ssize_t, ExceptionInfo *);
DEFINE_GVL_STUB4(DrawAffineImage, Image *, const Image *, const AffineMatrix *, ExceptionInfo *);
DEFINE_GVL_STUB2(EqualizeImage, Image *, ExceptionInfo *);
DEFINE_GVL_STUB4(EvaluateImage, Image *, const MagickEvaluateOperator, const double, ExceptionInfo *);
DEFINE_GVL_STUB7(FloodfillPaintImage, Image *, const DrawInfo *, const PixelInfo *, const ssize_t, const ssize_t, const MagickBooleanType, ExceptionInfo *);
DEFINE_GVL_STUB4(FrameImage, const Image *, const FrameInfo *, const CompositeOperator, ExceptionInfo *);
DEFINE_GVL_STUB5(FunctionImage, Image *, const MagickFunction, const size_t, const double *, ExceptionInfo *);
DEFINE_GVL_STUB3(FxImage, const Image *, const char *, ExceptionInfo *);
DEFINE_GVL_STUB3(GammaImage, Image *, const double, ExceptionInfo *);
DEFINE_GVL_STUB5(GetImageDistortion, Image *, const Image *, const MetricType, double *, ExceptionInfo *);
DEFINE_GVL_STUB3(GetImageEntropy, const Image *, double *, ExceptionInfo *);
DEFINE_GVL_STUB4(GetImageExtrema, const Image *, size_t *, size_t *, ExceptionInfo *);
DEFINE_GVL_STUB3(GetImageMask, const Image *, const PixelMask, ExceptionInfo *);
DEFINE_GVL_STUB4(GetImageMean, const Image *, double *, double *, ExceptionInfo *);
DEFINE_GVL_STUB4(ImplodeImage, const Image *, const double, const PixelInterpolateMethod, ExceptionInfo *);
DEFINE_GVL_STUB9(ImportImagePixels, Image *, const ssize_t, const ssize_t, const size_t, const size_t, const char *, const StorageType, const void *, ExceptionInfo *);
DEFINE_GVL_STUB5(IsEquivalentImage, const Image *, const Image *, ssize_t *, ssize_t *, ExceptionInfo *);
DEFINE_GVL_STUB5(LevelImage, Image *, const double, const double, const double, ExceptionInfo *);
DEFINE_GVL_STUB5(LevelImageColors, Image *, const PixelInfo *, const PixelInfo *, const MagickBooleanType, ExceptionInfo *);
DEFINE_GVL_STUB5(LevelizeImage, Image *, const double, const double, const double, ExceptionInfo *);
DEFINE_GVL_STUB4(LinearStretchImage, Image *, const double, const double, ExceptionInfo *);
DEFINE_GVL_STUB3(ModulateImage, Image *, const char *, ExceptionInfo *);
DEFINE_GVL_STUB5(MorphologyImage, const Image *, const MorphologyMethod, const ssize_t, const KernelInfo *, ExceptionInfo *);
DEFINE_GVL_STUB3(NegateImage, Image *, const MagickBooleanType, ExceptionInfo *);
DEFINE_GVL_STUB2(NormalizeImage, Image *, ExceptionInfo *);
DEFINE_GVL_STUB4(OilPaintImage, const Image *, const double, const double, ExceptionInfo *);
DEFINE_GVL_STUB5(OpaquePaintImage, Image *, const PixelInfo *, const PixelInfo *, const MagickBooleanType, ExceptionInfo *);
DEFINE_GVL_STUB3(OrderedDitherImage, Image *, const char *, ExceptionInfo *);
DEFINE_GVL_STUB6(PolaroidImage, const Image *, const DrawInfo *, const char *, const double, const PixelInterpolateMethod, ExceptionInfo *);
DEFINE_GVL_STUB4(PosterizeImage, Image *, const size_t, const DitherMethod, ExceptionInfo *);
DEFINE_GVL_STUB5(ProfileImage, Image *, const char *, const void *, const size_t, ExceptionInfo *);
DEFINE_GVL_STUB3(QuantizeImage, const QuantizeInfo *, Image *, ExceptionInfo *);
DEFINE_GVL_STUB4(RaiseImage, Image *, const RectangleInfo *, const MagickBooleanType, ExceptionInfo *);
DEFINE_GVL_STUB4(RandomThresholdImage, Image *, const double, const double, ExceptionInfo *);
DEFINE_GVL_STUB4(RemapImage, const QuantizeInfo *, Image *, const Image *, ExceptionInfo *);
DEFINE_GVL_STUB5(ResampleImage, const Image *, const double, const double, const FilterType, ExceptionInfo *);
DEFINE_GVL_STUB5(ResizeImage, const Image *, const size_t, const size_t, const FilterType, ExceptionInfo *);
DEFINE_GVL_STUB6(SegmentImage, Image *, const ColorspaceType, const MagickBooleanType, const double, const double, ExceptionInfo *);
DEFINE_GVL_STUB5(SelectiveBlurImage, const Image *, const double, const double, const double, ExceptionInfo *);
DEFINE_GVL_STUB3(SeparateImage, const Image *, const ChannelType, ExceptionInfo *);
DEFINE_GVL_STUB2(SeparateImages, const Image *, ExceptionInfo *);
DEFINE_GVL_STUB3(SetImageAlphaChannel, Image *, const AlphaChannelOption, ExceptionInfo *);
DEFINE_GVL_STUB2(SetImageBackgroundColor, Image *, ExceptionInfo *);
DEFINE_GVL_STUB3(SetImageDepth, Image *, const size_t, ExceptionInfo *);
DEFINE_GVL_STUB4(SetImageExtent, Image *, const size_t, const size_t, ExceptionInfo *);
DEFINE_GVL_STUB4(SetImageMask, Image *, const PixelMask, const Image *, ExceptionInfo *);
DEFINE_GVL_STUB3(SetImageStorageClass, Image *, const ClassType, ExceptionInfo *);
DEFINE_GVL_STUB5(SigmoidalContrastImage, Image *, const MagickBooleanType, const double, const double, ExceptionInfo *);
DEFINE_GVL_STUB2(SignatureImage, Image *, ExceptionInfo *);
DEFINE_GVL_STUB3(SolarizeImage, Image *, const double, ExceptionInfo *);
DEFINE_GVL_STUB5(SparseColorImage, const Image *, const SparseColorMethod, const size_t, const double *, ExceptionInfo *);
DEFINE_GVL_STUB4(SpreadImage, const Image *, const PixelInterpolateMethod, const double, ExceptionInfo *);
DEFINE_GVL_STUB4(SwirlImage, const Image *, double, const PixelInterpolateMethod, ExceptionInfo *);
DEFINE_GVL_STUB2(SyncImage, Image *, ExceptionInfo *);
DEFINE_GVL_STUB4(TintImage, const Image *, const char *, const PixelInfo *, ExceptionInfo *);
DEFINE_GVL_STUB3(TransformImageColorspace, Image *, const ColorspaceType, ExceptionInfo *);
DEFINE_GVL_STUB5(TransparentPaintImage, Image *, const PixelInfo *, const Quantum, const MagickBooleanType, ExceptionInfo *);
DEFINE_GVL_STUB6(TransparentPaintImageChroma, Image *, const PixelInfo *, const PixelInfo *, const Quantum, const MagickBooleanType, ExceptionInfo *);
DEFINE_GVL_STUB5(WaveImage, const Image *, const double, const double, const PixelInterpolateMethod, ExceptionInfo *);
DEFINE_GVL_STUB3(WhiteThresholdImage, Image *, const char *, ExceptionInfo *);
DEFINE_GVL_STUB3(WriteImage, const ImageInfo *, Image *, ExceptionInfo *);
#else
DEFINE_GVL_STUB5(AdaptiveBlurImageChannel, const Image *, const ChannelType, const double, const double, ExceptionInfo *);
DEFINE_GVL_STUB5(AdaptiveSharpenImageChannel, const Image *, const ChannelType, const double, const double, ExceptionInfo *);
DEFINE_GVL_STUB3(AddNoiseImage, const Image *, const NoiseType, ExceptionInfo *);
DEFINE_GVL_STUB4(AddNoiseImageChannel, const Image *, const ChannelType, const NoiseType, ExceptionInfo *);
DEFINE_GVL_STUB2(AutoGammaImageChannel, Image *, const ChannelType);
DEFINE_GVL_STUB2(AutoLevelImageChannel,Image *, const ChannelType);
DEFINE_GVL_STUB3(BilevelImageChannel, Image *, const ChannelType,const double);
DEFINE_GVL_STUB2(BlackThresholdImage, Image *, const char *);
DEFINE_GVL_STUB5(BlurImageChannel, const Image *, const ChannelType, const double, const double, ExceptionInfo *);
DEFINE_GVL_STUB3(BorderImage, const Image *, const RectangleInfo *, ExceptionInfo *);
DEFINE_GVL_STUB3(ClutImageChannel, Image *, const ChannelType, const Image *);
DEFINE_GVL_STUB4(ColorizeImage, const Image *, const char *, const PixelPacket, ExceptionInfo *);
DEFINE_GVL_STUB6(CompareImageChannels, Image *, const Image *, const ChannelType, const MetricType, double *, ExceptionInfo *);
DEFINE_GVL_STUB5(CompositeImage, Image *, const CompositeOperator, const Image *, const ssize_t, const ssize_t);
DEFINE_GVL_STUB6(CompositeImageChannel, Image *, const ChannelType, const CompositeOperator, const Image *, const ssize_t, const ssize_t);
DEFINE_GVL_STUB1(CompressImageColormap, Image *);
DEFINE_GVL_STUB2(ContrastImage, Image *, const MagickBooleanType);
DEFINE_GVL_STUB4(ContrastStretchImageChannel, Image *, const ChannelType, const double, const double);
DEFINE_GVL_STUB4(ConvolveImage, const Image *, const size_t, const double *, ExceptionInfo *);
DEFINE_GVL_STUB5(ConvolveImageChannel, const Image *, const ChannelType, const size_t, const double *, ExceptionInfo *);
DEFINE_GVL_STUB2(CycleColormapImage, Image *, const ssize_t);
DEFINE_GVL_STUB3(DrawAffineImage, Image *, const Image *, const AffineMatrix *);
DEFINE_GVL_STUB1(EqualizeImage, Image *);
DEFINE_GVL_STUB2(EqualizeImageChannel, Image *, const ChannelType);
DEFINE_GVL_STUB5(EvaluateImageChannel, Image *, const ChannelType, const MagickEvaluateOperator, const double, ExceptionInfo *);
DEFINE_GVL_STUB7(FloodfillPaintImage, Image *, const ChannelType, const DrawInfo *, const MagickPixelPacket *, const ssize_t, const ssize_t, const MagickBooleanType);
DEFINE_GVL_STUB3(FrameImage, const Image *, const FrameInfo *, ExceptionInfo *);
DEFINE_GVL_STUB6(FunctionImageChannel, Image *, const ChannelType, const MagickFunction, const size_t, const double *, ExceptionInfo *);
DEFINE_GVL_STUB4(FxImageChannel, const Image *, const ChannelType, const char *, ExceptionInfo *);
DEFINE_GVL_STUB3(GammaImageChannel, Image *, const ChannelType, const double);
DEFINE_GVL_STUB5(GaussianBlurImageChannel, const Image *, const ChannelType, const double, const double, ExceptionInfo *);
DEFINE_GVL_STUB3(GetImageChannelDepth, const Image *, const ChannelType, ExceptionInfo *);
DEFINE_GVL_STUB6(GetImageChannelDistortion, Image *, const Image *, const ChannelType, const MetricType, double *, ExceptionInfo *);
DEFINE_GVL_STUB4(GetImageChannelEntropy, const Image *, const ChannelType, double *, ExceptionInfo *);
DEFINE_GVL_STUB5(GetImageChannelExtrema, const Image *, const ChannelType, size_t *, size_t *, ExceptionInfo *);
DEFINE_GVL_STUB5(GetImageChannelMean, const Image *, const ChannelType, double *, double *, ExceptionInfo *);
DEFINE_GVL_STUB2(GetImageClipMask, const Image *, ExceptionInfo *);
DEFINE_GVL_STUB3(ImplodeImage, const Image *, const double, ExceptionInfo *);
DEFINE_GVL_STUB8(ImportImagePixels, Image *, const ssize_t, const ssize_t, const size_t,const size_t,const char *, const StorageType, const void *);
DEFINE_GVL_STUB5(IsImageSimilar, const Image *, const Image *, ssize_t *, ssize_t *, ExceptionInfo *);
DEFINE_GVL_STUB2(IsImagesEqual, Image *, const Image *);
DEFINE_GVL_STUB5(LevelColorsImageChannel, Image *, const ChannelType, const MagickPixelPacket *, const MagickPixelPacket *, const MagickBooleanType);
DEFINE_GVL_STUB2(LevelImage, Image *, const char *);
DEFINE_GVL_STUB5(LevelImageChannel, Image *, const ChannelType, const double, const double, const double);
DEFINE_GVL_STUB5(LevelizeImageChannel, Image *, const ChannelType, const double, const double, const double);
DEFINE_GVL_STUB3(LinearStretchImage, Image *, const double, const double);
DEFINE_GVL_STUB2(ModulateImage, Image *, const char *);
DEFINE_GVL_STUB6(MorphologyImageChannel, const Image *, const ChannelType, const MorphologyMethod, const ssize_t, const KernelInfo *, ExceptionInfo *);
DEFINE_GVL_STUB2(NegateImage, Image *, const MagickBooleanType);
DEFINE_GVL_STUB3(NegateImageChannel, Image *, const ChannelType, const MagickBooleanType);
DEFINE_GVL_STUB1(NormalizeImage, Image *);
DEFINE_GVL_STUB2(NormalizeImageChannel, Image *, const ChannelType);
DEFINE_GVL_STUB3(OilPaintImage, const Image *, const double, ExceptionInfo *);
DEFINE_GVL_STUB5(OpaquePaintImageChannel, Image *, const ChannelType, const MagickPixelPacket *, const MagickPixelPacket *, const MagickBooleanType);
DEFINE_GVL_STUB3(OrderedPosterizeImage, Image *, const char *, ExceptionInfo *);
DEFINE_GVL_STUB4(PolaroidImage, const Image *, const DrawInfo *, const double, ExceptionInfo *);
DEFINE_GVL_STUB3(PosterizeImage, Image *, const size_t, const MagickBooleanType);
DEFINE_GVL_STUB5(ProfileImage, Image *, const char *, const void *, const size_t, const MagickBooleanType);
DEFINE_GVL_STUB2(QuantizeImage, const QuantizeInfo *, Image *);
DEFINE_GVL_STUB3(RaiseImage, Image *, const RectangleInfo *, const MagickBooleanType);
DEFINE_GVL_STUB4(RandomThresholdImageChannel, Image *, const ChannelType, const char *, ExceptionInfo *);
DEFINE_GVL_STUB3(RemapImage, const QuantizeInfo *, Image *, const Image *);
DEFINE_GVL_STUB6(ResampleImage, const Image *, const double, const double, const FilterTypes, const double, ExceptionInfo *);
DEFINE_GVL_STUB6(ResizeImage, const Image *, const size_t, const size_t, const FilterTypes, const double, ExceptionInfo *);
DEFINE_GVL_STUB5(SegmentImage, Image *, const ColorspaceType, const MagickBooleanType, const double, const double);
DEFINE_GVL_STUB6(SelectiveBlurImageChannel, const Image *, const ChannelType, const double, const double,  const double, ExceptionInfo *);
DEFINE_GVL_STUB2(SeparateImageChannel, Image *, const ChannelType);
DEFINE_GVL_STUB3(SeparateImages, const Image *, const ChannelType, ExceptionInfo *);
DEFINE_GVL_STUB2(SetImageAlphaChannel, Image *, const AlphaChannelType);
DEFINE_GVL_STUB1(SetImageBackgroundColor, Image *);
DEFINE_GVL_STUB3(SetImageChannelDepth, Image *, const ChannelType, const size_t);
DEFINE_GVL_STUB2(SetImageClipMask, Image *, const Image *);
DEFINE_GVL_STUB2(SetImageDepth, Image *, const size_t);
DEFINE_GVL_STUB3(SetImageExtent, Image *, const size_t, const size_t);
DEFINE_GVL_STUB2(SetImageMask, Image *, const Image *);
DEFINE_GVL_STUB2(SetImageStorageClass, Image *, const ClassType);
DEFINE_GVL_STUB5(SharpenImageChannel, const Image *, const ChannelType, const double, const double,  ExceptionInfo *);
DEFINE_GVL_STUB5(SigmoidalContrastImageChannel, Image *, const ChannelType, const MagickBooleanType, const double, const double);
DEFINE_GVL_STUB1(SignatureImage, Image *);
DEFINE_GVL_STUB2(SolarizeImage, Image *, const double);
DEFINE_GVL_STUB6(SparseColorImage, const Image *, const ChannelType, const SparseColorMethod, const size_t, const double *, ExceptionInfo *);
DEFINE_GVL_STUB3(SpreadImage, const Image *, const double, ExceptionInfo *);
DEFINE_GVL_STUB3(SwirlImage, const Image *, double, ExceptionInfo *);
DEFINE_GVL_STUB1(SyncImage, Image *);
DEFINE_GVL_STUB4(TintImage, const Image *, const char *, const PixelPacket, ExceptionInfo *);
DEFINE_GVL_STUB2(TransformImageColorspace, Image *, const ColorspaceType);
DEFINE_GVL_STUB4(TransparentPaintImage, Image *, const MagickPixelPacket *, const Quantum, const MagickBooleanType);
DEFINE_GVL_STUB5(TransparentPaintImageChroma, Image *, const MagickPixelPacket *, const MagickPixelPacket *, const Quantum, const MagickBooleanType);
DEFINE_GVL_STUB7(UnsharpMaskImageChannel, const Image *, const ChannelType, const double, const double,  const double, const double, ExceptionInfo *);
DEFINE_GVL_STUB4(WaveImage, const Image *, const double, const double, ExceptionInfo *);
DEFINE_GVL_STUB2(WhiteThresholdImage, Image *, const char *);
DEFINE_GVL_STUB2(WriteImage, const ImageInfo *, Image *);
#endif

DEFINE_GVL_STUB3(RotationalBlurImage, const Image *, const double, ExceptionInfo *);

#if defined(IMAGEMAGICK_7)
#else
DEFINE_GVL_STUB4(RotationalBlurImageChannel, const Image *, const ChannelType, const double, ExceptionInfo *);
#endif

/**
 * Returns the alpha value from the hash.
 *
 * No Ruby usage (internal function)
 *
 * @kwargs the keyword arguments
 */
static Quantum
get_named_alpha_value(VALUE kwargs)
{
    ID keyword_ids[] = { rb_intern("alpha") };
    VALUE alpha;

    rb_get_kwargs(kwargs, keyword_ids, 1, 0, &alpha);
    if (NIL_P(alpha))
    {
        rb_raise(rb_eArgError, "missing keyword: alpha");
    }

    return APP2QUANTUM(alpha);
}


// aliases for common use of structure types; AdaptiveBlurImage, AdaptiveSharpenImage
typedef GVL_STRUCT_TYPE(AdaptiveBlurImage) GVL_STRUCT_TYPE(adaptive_method);

/**
 * Call Adaptive(Blur|Sharpen)Image.
 *
 * No Ruby usage (internal function)
 *
 * @param argc number of input arguments
 * @param argv array of input arguments
 * @param self this object
 * @param fp pointer to the function to call
 * @return a new image
 */
static VALUE
adaptive_method(int argc, VALUE *argv, VALUE self, gvl_function_t fp)
{
    Image *image, *new_image;
    double radius = 0.0;
    double sigma = 1.0;
    ExceptionInfo *exception;

    image = rm_check_destroyed(self);

    switch (argc)
    {
        case 2:
            sigma = NUM2DBL(argv[1]);
        case 1:
            radius = NUM2DBL(argv[0]);
        case 0:
            break;
        default:
            rb_raise(rb_eArgError, "wrong number of arguments (%d for 0 to 2)", argc);
            break;
    }

    exception = AcquireExceptionInfo();

    GVL_STRUCT_TYPE(adaptive_method) args = { image, radius, sigma, exception };
    new_image = (Image *)CALL_FUNC_WITHOUT_GVL(fp, &args);
    rm_check_exception(exception, new_image, DestroyOnError);
    DestroyExceptionInfo(exception);

    return rm_image_new(new_image);
}


// aliases for common use of structure types; AdaptiveBlurImage, AdaptiveSharpenImage, AdaptiveBlurImageChannel, AdaptiveSharpenImageChannel
#if defined(IMAGEMAGICK_7)
typedef GVL_STRUCT_TYPE(AdaptiveBlurImage) GVL_STRUCT_TYPE(adaptive_channel_method);
#else
typedef GVL_STRUCT_TYPE(AdaptiveBlurImageChannel) GVL_STRUCT_TYPE(adaptive_channel_method);
#endif

/**
 * Call Adaptive(Blur|Sharpen)ImageChannel.
 *
 * No Ruby usage (internal function)
 *
 * @param argc number of input arguments
 * @param argv array of input arguments
 * @param self this object
 * @param fp pointer to the function to call
 * @return a new image
 */
static VALUE
adaptive_channel_method(int argc, VALUE *argv, VALUE self, gvl_function_t fp)
{
    Image *image, *new_image;
    double radius = 0.0;
    double sigma = 1.0;
    ExceptionInfo *exception;
    ChannelType channels;

    image = rm_check_destroyed(self);
    channels = extract_channels(&argc, argv);

    switch (argc)
    {
        case 2:
            sigma = NUM2DBL(argv[1]);
        case 1:
            radius = NUM2DBL(argv[0]);
        case 0:
            break;
        default:
            raise_ChannelType_error(argv[argc-1]);
            break;
    }

    exception = AcquireExceptionInfo();

#if defined(IMAGEMAGICK_7)
    BEGIN_CHANNEL_MASK(image, channels);
    GVL_STRUCT_TYPE(adaptive_channel_method) args = { image, radius, sigma, exception };
    new_image = (Image *)CALL_FUNC_WITHOUT_GVL(fp, &args);
    CHANGE_RESULT_CHANNEL_MASK(new_image);
    END_CHANNEL_MASK(image);
#else
    GVL_STRUCT_TYPE(adaptive_channel_method) args = { image, channels, radius, sigma, exception };
    new_image = (Image *)CALL_FUNC_WITHOUT_GVL(fp, &args);
#endif

    rm_check_exception(exception, new_image, DestroyOnError);
    DestroyExceptionInfo(exception);

    return rm_image_new(new_image);
}


/**
 * Adaptively blurs the image by blurring more intensely near image edges and less intensely far
 * from edges. The {Magick::Image#adaptive_blur} method blurs the image with a Gaussian operator of
 * the given radius and standard deviation (sigma). For reasonable results, radius should be larger
 * than sigma. Use a radius of 0 and adaptive_blur selects a suitable radius for you.
 *
 * @overload adaptive_blur(radius = 0.0, sigma = 1.0)
 *   @param radius [Numeric] The radius of the Gaussian in pixels, not counting the center pixel.
 *   @param sigma [Numeric] The standard deviation of the Laplacian, in pixels.
 *   @return [Magick::Image] a new image
 */
VALUE
Image_adaptive_blur(int argc, VALUE *argv, VALUE self)
{
    return adaptive_method(argc, argv, self, GVL_FUNC(AdaptiveBlurImage));
}


/**
 * The same as {Magick::Image#adaptive_blur} except only the specified channels are blurred.
 *
 * @overload adaptive_blur_channel(radius = 0.0, sigma = 1.0, channel = Magick::AllChannels)
 *   @param radius [Numeric] The radius of the Gaussian in pixels, not counting the center pixel.
 *   @param sigma [Numeric] The standard deviation of the Laplacian, in pixels.
 *   @param channel [Magick::ChannelType] a ChannelType arguments.
 *
 * @overload adaptive_blur_channel(radius = 0.0, sigma = 1.0, *channels)
 *   @param radius [Numeric] The radius of the Gaussian in pixels, not counting the center pixel.
 *   @param sigma [Numeric] The standard deviation of the Laplacian, in pixels.
 *   @param *channels [Magick::ChannelType] one or more ChannelType arguments.
 *
 * @return [Magick::Image] a new image
 */
VALUE
Image_adaptive_blur_channel(int argc, VALUE *argv, VALUE self)
{
#if defined(IMAGEMAGICK_7)
    return adaptive_channel_method(argc, argv, self, GVL_FUNC(AdaptiveBlurImage));
#else
    return adaptive_channel_method(argc, argv, self, GVL_FUNC(AdaptiveBlurImageChannel));
#endif
}


/**
 * Resizes the image with data dependent triangulation.
 *
 * @overload adaptive_resize(scale_val)
 *   @param scale_val [Numeric] You can use this argument instead of specifying the desired width and
 *     height. The percentage size change. For example, 1.25 makes the new image 125% of the size of
 *     the receiver.
 *
 * @overload adaptive_resize(cols, rows)
 *   @param cols [Numeric] The desired column size
 *   @param rows [Numeric] The desired row size.
 *
 * @return [Magick::Image] a new image
 */
VALUE
Image_adaptive_resize(int argc, VALUE *argv, VALUE self)
{
    Image *image, *new_image;
    unsigned long rows, columns;
    double scale_val, drows, dcols;
    ExceptionInfo *exception;

    image = rm_check_destroyed(self);

    switch (argc)
    {
        case 2:
            rows = NUM2ULONG(argv[1]);
            columns = NUM2ULONG(argv[0]);
            break;
        case 1:
            scale_val = NUM2DBL(argv[0]);
            if (scale_val < 0.0)
            {
                rb_raise(rb_eArgError, "invalid scale_val value (%g given)", scale_val);
            }
            drows = scale_val * image->rows + 0.5;
            dcols = scale_val * image->columns + 0.5;
            if (drows > (double)ULONG_MAX || dcols > (double)ULONG_MAX)
            {
                rb_raise(rb_eRangeError, "resized image too big");
            }
            rows = (unsigned long) drows;
            columns = (unsigned long) dcols;
            break;
        default:
            rb_raise(rb_eArgError, "wrong number of arguments (%d for 1 or 2)", argc);
            break;
    }

    exception = AcquireExceptionInfo();
    GVL_STRUCT_TYPE(AdaptiveResizeImage) args = { image, columns, rows, exception };
    new_image = (Image *)CALL_FUNC_WITHOUT_GVL(GVL_FUNC(AdaptiveResizeImage), &args);
    rm_check_exception(exception, new_image, DestroyOnError);
    DestroyExceptionInfo(exception);

    return rm_image_new(new_image);
}


/**
 * Adaptively sharpens the image by sharpening more intensely near image edges and less intensely
 * far from edges.
 *
 * The {Magick::Image#adaptive_sharpen} method sharpens the image with a Gaussian operator of the
 * given radius and standard deviation (sigma).
 *
 * For reasonable results, radius should be larger than sigma.
 * Use a radius of 0 and adaptive_sharpen selects a suitable radius for you.
 *
 * @overload adaptive_sharpen(radius = 0.0, sigma = 1.0)
 *   @param radius [Numeric] The radius of the Gaussian in pixels, not counting the center pixel.
 *   @param sigma [Numeric] The standard deviation of the Laplacian, in pixels.
 *   @return [Magick::Image] a new image
 */
VALUE
Image_adaptive_sharpen(int argc, VALUE *argv, VALUE self)
{
    return adaptive_method(argc, argv, self, GVL_FUNC(AdaptiveSharpenImage));
}


/**
 * The same as {Magick::Image#adaptive_sharpen} except only the specified channels are sharpened.
 *
 * @overload adaptive_sharpen_channel(radius = 0.0, sigma = 1.0, channel = Magick::AllChannels)
 *   @param radius [Numeric] The radius of the Gaussian in pixels, not counting the center pixel.
 *   @param sigma [Numeric] The standard deviation of the Laplacian, in pixels.
 *   @param channel [Magick::ChannelType] a ChannelType arguments.
 *
 * @overload adaptive_sharpen_channel(radius = 0.0, sigma = 1.0, *channels)
 *   @param radius [Numeric] The radius of the Gaussian in pixels, not counting the center pixel.
 *   @param sigma [Numeric] The standard deviation of the Laplacian, in pixels.
 *   @param *channels [Magick::ChannelType] one or more ChannelType arguments.
 *
 * @return [Magick::Image] a new image
 */
VALUE
Image_adaptive_sharpen_channel(int argc, VALUE *argv, VALUE self)
{
#if defined(IMAGEMAGICK_7)
    return adaptive_channel_method(argc, argv, self, GVL_FUNC(AdaptiveSharpenImage));
#else
    return adaptive_channel_method(argc, argv, self, GVL_FUNC(AdaptiveSharpenImageChannel));
#endif
}


/**
 * Selects an individual threshold for each pixel based on the range of intensity values in its
 * local neighborhood. This allows for thresholding of an image whose global intensity histogram
 * doesn't contain distinctive peaks.
 *
 * @overload adaptive_threshold(width = 3, height = 3, bias = 0)
 *   @param width [Numeric] the width of the local neighborhood.
 *   @param height [Numeric] the height of the local neighborhood.
 *   @param bias [Numeric] the mean offset
 *   @return [Magick::Image] a new image
 */
VALUE
Image_adaptive_threshold(int argc, VALUE *argv, VALUE self)
{
    Image *image, *new_image;
    unsigned long width = 3, height = 3;
    double bias = 0;
    ExceptionInfo *exception;

    image = rm_check_destroyed(self);

    switch (argc)
    {
        case 3:
            bias = NUM2DBL(argv[2]);
        case 2:
            height = NUM2ULONG(argv[1]);
        case 1:
            width  = NUM2ULONG(argv[0]);
        case 0:
            break;
        default:
            rb_raise(rb_eArgError, "wrong number of arguments (%d for 0 to 3)", argc);
    }

    exception = AcquireExceptionInfo();
    GVL_STRUCT_TYPE(AdaptiveThresholdImage) args = { image, width, height, bias, exception };
    new_image = (Image *)CALL_FUNC_WITHOUT_GVL(GVL_FUNC(AdaptiveThresholdImage), &args);
    rm_check_exception(exception, new_image, DestroyOnError);
    DestroyExceptionInfo(exception);

    return rm_image_new(new_image);
}


/**
 * Associates a mask with an image that will be used as the destination image in a
 * {Magick::Image#composite} operation.
 *
 * The areas of the destination image that are masked by white pixels will be modified by the
 * {Magick::Image#composite} method, while areas masked by black pixels are unchanged.
 *
 * @param mask [Magick::Image, Magick::ImageList] the composite mask
 * @see Image#mask
 * @see Image#delete_compose_mask
 */
VALUE
Image_add_compose_mask(VALUE self, VALUE mask)
{
    Image *image, *mask_image = NULL;
#if defined(IMAGEMAGICK_7)
    ExceptionInfo *exception;
    Image *clip_mask = NULL;
#endif

    image = rm_check_frozen(self);
    mask_image = rm_check_destroyed(rm_cur_image(mask));
    if (image->columns != mask_image->columns || image->rows != mask_image->rows)
    {
        rb_raise(rb_eArgError, "mask must be the same size as image");
    }

#if defined(IMAGEMAGICK_7)
    clip_mask = rm_clone_image(mask_image);

    exception = AcquireExceptionInfo();
    GVL_STRUCT_TYPE(NegateImage) args_NegateImage = { clip_mask, MagickFalse, exception };
    CALL_FUNC_WITHOUT_GVL(GVL_FUNC(NegateImage), &args_NegateImage);
    rm_check_exception(exception, clip_mask, DestroyOnError);
    GVL_STRUCT_TYPE(SetImageMask) args_SetImageMask = { image, CompositePixelMask, clip_mask, exception };
    CALL_FUNC_WITHOUT_GVL(GVL_FUNC(SetImageMask), &args_SetImageMask);
    DestroyImage(clip_mask);
    CHECK_EXCEPTION();
    DestroyExceptionInfo(exception);
#else
    // Delete any previously-existing mask image.
    // Store a clone of the new mask image.
    GVL_STRUCT_TYPE(SetImageMask) args_SetImageMask = { image, mask_image };
    CALL_FUNC_WITHOUT_GVL(GVL_FUNC(SetImageMask), &args_SetImageMask);
    GVL_STRUCT_TYPE(NegateImage) args_NegateImage = { image->mask, MagickFalse };
    CALL_FUNC_WITHOUT_GVL(GVL_FUNC(NegateImage), &args_NegateImage);

    // Since both Set and GetImageMask clone the mask image I don't see any
    // way to negate the mask without referencing it directly. Sigh.
#endif

    return self;
}


/**
 * Adds random noise to the image.
 *
 * @param noise [Magick::NoiseType] the noise
 * @return [Magick::Image] a new image
 */
VALUE
Image_add_noise(VALUE self, VALUE noise)
{
    Image *image, *new_image;
    NoiseType noise_type;
    ExceptionInfo *exception;

    image = rm_check_destroyed(self);

    VALUE_TO_ENUM(noise, noise_type, NoiseType);

    exception = AcquireExceptionInfo();
#if defined(IMAGEMAGICK_7)
    GVL_STRUCT_TYPE(AddNoiseImage) args = { image, noise_type, 1.0, exception };
#else
    GVL_STRUCT_TYPE(AddNoiseImage) args = { image, noise_type, exception };
#endif
    new_image = (Image *)CALL_FUNC_WITHOUT_GVL(GVL_FUNC(AddNoiseImage), &args);
    rm_check_exception(exception, new_image, DestroyOnError);
    DestroyExceptionInfo(exception);

    return rm_image_new(new_image);
}

/**
 * Adds random noise to the specified channel or channels in the image.
 *
 * @overload add_noise_channel(noise_type, channel = Magick::AllChannels)
 *   @param noise [Magick::NoiseType] the noise
 *   @param channel [Magick::ChannelType] a ChannelType arguments.
 *
 * @overload add_noise_channel(noise_type, *channels)
 *   @param noise [Magick::NoiseType] the noise
 *   @param *channels [Magick::ChannelType] one or more ChannelType arguments.
 *
 * @return [Magick::Image] a new image
 */
VALUE
Image_add_noise_channel(int argc, VALUE *argv, VALUE self)
{
    Image *image, *new_image;
    NoiseType noise_type;
    ExceptionInfo *exception;
    ChannelType channels;

    image = rm_check_destroyed(self);
    channels = extract_channels(&argc, argv);

    // There must be 1 remaining argument.
    if (argc == 0)
    {
        rb_raise(rb_eArgError, "missing noise type argument");
    }
    else if (argc > 1)
    {
        raise_ChannelType_error(argv[argc-1]);
    }

    VALUE_TO_ENUM(argv[0], noise_type, NoiseType);
    channels = (ChannelType)(channels & ~OpacityChannel);

    exception = AcquireExceptionInfo();
#if defined(IMAGEMAGICK_7)
    BEGIN_CHANNEL_MASK(image, channels);
    GVL_STRUCT_TYPE(AddNoiseImage) args = { image, noise_type, 1.0, exception };
    new_image = (Image *)CALL_FUNC_WITHOUT_GVL(GVL_FUNC(AddNoiseImage), &args);
    END_CHANNEL_MASK(new_image);
#else
    GVL_STRUCT_TYPE(AddNoiseImageChannel) args = { image, channels, noise_type, exception };
    new_image = (Image *)CALL_FUNC_WITHOUT_GVL(GVL_FUNC(AddNoiseImageChannel), &args);
#endif
    rm_check_exception(exception, new_image, DestroyOnError);
    DestroyExceptionInfo(exception);

    return rm_image_new(new_image);
}


/**
 * Adds an ICC (a.k.a. ICM), IPTC, or generic profile. If the file contains more than one profile
 * all the profiles are added.
 *
 * @param name [String] The filename of a file containing the profile.
 * @return [Magick::Image] self
 */
VALUE
Image_add_profile(VALUE self, VALUE name)
{
    // ImageMagick code based on the code for the "-profile" option in mogrify.c
    Image *image, *profile_image;
    ImageInfo *info;
    ExceptionInfo *exception;
    char *profile_name;
    char *profile_filename = NULL;
    const StringInfo *profile;

    image = rm_check_frozen(self);

    // ProfileImage issues a warning if something goes wrong.
    profile_filename = StringValueCStr(name);

    info = CloneImageInfo(NULL);
    if (!info)
    {
        rb_raise(rb_eNoMemError, "not enough memory to continue");
    }
    profile = GetImageProfile(image, "iptc");
    if (profile)
    {
        info->profile = (void *)CloneStringInfo(profile);
    }
    strlcpy(info->filename, profile_filename, sizeof(info->filename));

    exception = AcquireExceptionInfo();
    profile_image = ReadImage(info, exception);
    DestroyImageInfo(info);
    rm_check_exception(exception, profile_image, DestroyOnError);
    rm_ensure_result(profile_image);

    ResetImageProfileIterator(profile_image);
    profile_name = GetNextImageProfile(profile_image);
    while (profile_name)
    {
        profile = GetImageProfile(profile_image, profile_name);
        if (profile)
        {
#if defined(IMAGEMAGICK_7)
            GVL_STRUCT_TYPE(ProfileImage) args = { image, profile_name, GetStringInfoDatum(profile), GetStringInfoLength(profile), exception };
            CALL_FUNC_WITHOUT_GVL(GVL_FUNC(ProfileImage), &args);
            if (rm_should_raise_exception(exception, RetainExceptionRetention))
#else
            GVL_STRUCT_TYPE(ProfileImage) args = { image, profile_name, GetStringInfoDatum(profile), GetStringInfoLength(profile), MagickFalse };
            CALL_FUNC_WITHOUT_GVL(GVL_FUNC(ProfileImage), &args);
            if (rm_should_raise_exception(&image->exception, RetainExceptionRetention))
#endif
            {
                break;
            }
        }
        profile_name = GetNextImageProfile(profile_image);
    }

    DestroyImage(profile_image);
#if defined(IMAGEMAGICK_7)
    CHECK_EXCEPTION();
    DestroyExceptionInfo(exception);
#else
    DestroyExceptionInfo(exception);
    rm_check_image_exception(image, RetainOnError);
#endif

    return self;
}



/**
 * Get/Set alpha channel.
 *
 * @overload alpha()
 *   Returns true if the alpha channel will be used, false otherwise.
 *   This calling is same as {Magick::Image#alpha?}.
 *   @return [Boolean] true or false
 *   @see Image#alpha?
 *
 * @overload alpha(value)
 *   Activates, deactivates, resets, or sets the alpha channel.
 *   @param value [Magick::AlphaChannelOption] An AlphaChannelOption value
 *   @return [Magick::AlphaChannelOption] the given value
 *
 * - Replaces {Magick::Image#matte=}, {Magick::Image#alpha=}
 * - Originally there was an alpha attribute getter and setter. These are replaced with alpha? and
 *   alpha(type). We still define (but don't document) alpha=. For backward compatibility, if this
 *   method is called without an argument, make it act like the old alpha getter and return true if
 *   the matte channel is active, false otherwise.
 *
 */
VALUE
Image_alpha(int argc, VALUE *argv, VALUE self)
{
    Image *image;
    AlphaChannelOption alpha;
#if defined(IMAGEMAGICK_7)
    ExceptionInfo *exception;
#endif


    // For backward compatibility, make alpha() act like alpha?
    if (argc == 0)
    {
        return Image_alpha_q(self);
    }
    else if (argc > 1)
    {
        rb_raise(rb_eArgError, "wrong number of arguments (%d for 1)", argc);
    }


    image = rm_check_frozen(self);
    VALUE_TO_ENUM(argv[0], alpha, AlphaChannelOption);

#if defined(IMAGEMAGICK_7)
    exception = AcquireExceptionInfo();
    GVL_STRUCT_TYPE(SetImageAlphaChannel) args = { image, alpha, exception };
    CALL_FUNC_WITHOUT_GVL(GVL_FUNC(SetImageAlphaChannel), &args);
    CHECK_EXCEPTION();
    DestroyExceptionInfo(exception);
#else
    GVL_STRUCT_TYPE(SetImageAlphaChannel) args = { image, alpha };
    CALL_FUNC_WITHOUT_GVL(GVL_FUNC(SetImageAlphaChannel), &args);
    rm_check_image_exception(image, RetainOnError);
#endif

    return argv[0];
}



/**
 * Determine whether the image's alpha channel is activated.
 *
 * @return [Boolean] true if the image's alpha channel is activated
 */
VALUE
Image_alpha_q(VALUE self)
{
    Image *image = rm_check_destroyed(self);
#if defined(IMAGEMAGICK_7)
    return image->alpha_trait == BlendPixelTrait ? Qtrue : Qfalse;
#else
    return GetImageAlphaChannel(image) ? Qtrue : Qfalse;
#endif
}


/**
 * Transform an image as dictated by the affine matrix argument.
 *
 * @param affine [Magick::AffineMatrix] the affine matrix
 * @return [Magick::Image] a new image
 */
VALUE
Image_affine_transform(VALUE self, VALUE affine)
{
    Image *image, *new_image;
    ExceptionInfo *exception;
    AffineMatrix matrix;

    image = rm_check_destroyed(self);

    // Convert Magick::AffineMatrix to AffineMatrix structure.
    Export_AffineMatrix(&matrix, affine);

    exception = AcquireExceptionInfo();
    GVL_STRUCT_TYPE(AffineTransformImage) args = { image, &matrix, exception };
    void *ret = CALL_FUNC_WITHOUT_GVL(GVL_FUNC(AffineTransformImage), &args);
    new_image = reinterpret_cast<decltype(new_image)>(ret);
    rm_check_exception(exception, new_image, DestroyOnError);
    DestroyExceptionInfo(exception);

    return rm_image_new(new_image);
}

/**
 * Returns the value of the image property identified by key. An image may have any number of
 * properties.
 *
 * Each property is identified by a string (or symbol) key.
 * The property value is a string. ImageMagick predefines some properties, including "Label",
 * "Comment", "Signature", and in some cases "EXIF".
 *
 * @param key_arg [String, Symbol] the key to get
 * @return [String, nil] property value or nil if key doesn't exist
 * @see Image#[]=
 * @see Image#properties
 */
VALUE
 Image_aref(VALUE self, VALUE key_arg)
{
    Image *image;
    const char *key;
    const char *attr;

    image = rm_check_destroyed(self);

    switch (TYPE(key_arg))
    {
        case T_NIL:
            return Qnil;

        case T_SYMBOL:
            key = rb_id2name((ID)SYM2ID(key_arg));
            break;

        default:
            key = StringValueCStr(key_arg);
            if (*key == '\0')
            {
                return Qnil;
            }
            break;
    }


    if (rm_strcasecmp(key, "EXIF:*") == 0)
    {
        return rm_exif_by_entry(image);
    }
    else if (rm_strcasecmp(key, "EXIF:!") == 0)
    {
        return rm_exif_by_number(image);
    }

    attr = rm_get_property(image, key);
    return attr ? rb_str_new2(attr) : Qnil;
}

/**
 * Sets the value of an image property. An image may have any number of properties.
 *
 * - Specify attr=nil to remove the key from the list.
 * - SetImageProperty normally APPENDS the new value to any existing value. Since this usage is
 *   tremendously counter-intuitive, this function always deletes the existing value before setting
 *   the new value.
 * - There's no use checking the return value since SetImageProperty returns "False" for many
 *   reasons, some legitimate.
 *
 * @param key_arg [String, Symbol] the key to set
 * @param attr_arg [String] the value to which to set it
 * @return [Magick::Image] self
 */
VALUE
Image_aset(VALUE self, VALUE key_arg, VALUE attr_arg)
{
    Image *image;
    const char *key;
    char *attr;
    unsigned int okay;

    image = rm_check_frozen(self);

    attr = attr_arg == Qnil ? NULL : StringValueCStr(attr_arg);

    switch (TYPE(key_arg))
    {
        case T_NIL:
            return self;

        case T_SYMBOL:
            key = rb_id2name((ID)SYM2ID(key_arg));
            break;

        default:
            key = StringValueCStr(key_arg);
            if (*key == '\0')
            {
                return self;
            }
            break;
    }


    // Delete existing value. SetImageProperty returns False if
    // the attribute doesn't exist - we don't care.
    rm_set_property(image, key, NULL);
    // Set new value
    if (attr)
    {
        okay = rm_set_property(image, key, attr);
        if (!okay)
        {
            rb_warning("SetImageProperty failed (probably out of memory)");
        }
    }
    return self;
}


// aliases for common use of structure types; TransposeImage, TransverseImage
typedef GVL_STRUCT_TYPE(TransposeImage) GVL_STRUCT_TYPE(crisscross);

/**
 * Handle #transverse, #transform methods.
 *
 * No Ruby usage (internal function)
 *
 * @param bang whether the bang (!) version of the method was called
 * @param self this object
 * @param fp the transverse/transform method to call
 * @return self if bang, otherwise a new image
 */
static VALUE
crisscross(int bang, VALUE self, gvl_function_t fp)
{
    Image *image, *new_image;
    ExceptionInfo *exception;

    TypedData_Get_Struct(self, Image, &rm_image_data_type, image);
    exception = AcquireExceptionInfo();

    GVL_STRUCT_TYPE(crisscross) args = { image, exception };
    void *ret = CALL_FUNC_WITHOUT_GVL(fp, &args);
    new_image = reinterpret_cast<decltype(new_image)>(ret);
    rm_check_exception(exception, new_image, DestroyOnError);
    DestroyExceptionInfo(exception);

    if (bang)
    {
        rm_ensure_result(new_image);
        UPDATE_DATA_PTR(self, new_image);
        rm_image_destroy(image);
        return self;
    }

    return rm_image_new(new_image);

}


// aliases for common use of structure types; AutoGammaImage, AutoLevelImage, AutoGammaImageChannel, AutoLevelImageChannel
#if defined(IMAGEMAGICK_7)
typedef GVL_STRUCT_TYPE(AutoGammaImage) GVL_STRUCT_TYPE(auto_channel);
#else
typedef GVL_STRUCT_TYPE(AutoGammaImageChannel) GVL_STRUCT_TYPE(auto_channel);
#endif

/**
 * Handle #auto_gamma_channel, #auto_level_channel methods.
 *
 * No Ruby usage (internal function)
 *
 * @param argc number of input arguments
 * @param argv array of input arguments
 * @param self this object
 * @param fp the channel method to call
 * @return a new image
 */
static VALUE
auto_channel(int argc, VALUE *argv, VALUE self, gvl_function_t fp)
{
    Image *image, *new_image;
    ChannelType channels;
#if defined(IMAGEMAGICK_7)
    ExceptionInfo *exception;
#endif

    image = rm_check_destroyed(self);
    channels = extract_channels(&argc, argv);

    if (argc > 0)
    {
        raise_ChannelType_error(argv[argc-1]);
    }

    new_image = rm_clone_image(image);

#if defined(IMAGEMAGICK_7)
    exception = AcquireExceptionInfo();
    BEGIN_CHANNEL_MASK(new_image, channels);
    GVL_STRUCT_TYPE(auto_channel) args = { new_image, exception };
    CALL_FUNC_WITHOUT_GVL(fp, &args);
    END_CHANNEL_MASK(new_image);
    rm_check_exception(exception, new_image, DestroyOnError);
    DestroyExceptionInfo(exception);
#else
    GVL_STRUCT_TYPE(auto_channel) args = { new_image, channels };
    CALL_FUNC_WITHOUT_GVL(fp, &args);
    rm_check_image_exception(new_image, DestroyOnError);
#endif

    return rm_image_new(new_image);
}


/**
 * "Automagically" adjust the gamma level of an image.
 *
 * @overload auto_gamma_channel(channel = Magick::AllChannels)
 *   @param channel [Magick::ChannelType] a ChannelType arguments.
 *
 * @overload auto_gamma_channel(*channels)
 *   @param *channels [Magick::ChannelType] one or more ChannelType arguments.
 *
 * @return [Magick::Image] a new image
 */
VALUE
Image_auto_gamma_channel(int argc, VALUE *argv, VALUE self)
{
#if defined(IMAGEMAGICK_7)
    return auto_channel(argc, argv, self, GVL_FUNC(AutoGammaImage));
#else
    return auto_channel(argc, argv, self, GVL_FUNC(AutoGammaImageChannel));
#endif
}


/**
 * "Automagically" adjust the color levels of an image.
 *
 * @overload auto_level_channel(channel = Magick::AllChannels)
 *   @param channel [Magick::ChannelType] a ChannelType arguments.
 *
 * @overload auto_level_channel(*channels)
 *   @param *channels [Magick::ChannelType] one or more ChannelType arguments.
 *
 * @return [Magick::Image] a new image
 */
VALUE
Image_auto_level_channel(int argc, VALUE *argv, VALUE self)
{
#if defined(IMAGEMAGICK_7)
    return auto_channel(argc, argv, self, GVL_FUNC(AutoLevelImage));
#else
    return auto_channel(argc, argv, self, GVL_FUNC(AutoLevelImageChannel));
#endif
}


/**
 * Implement mogrify's -auto_orient option automatically orient image based on
 * EXIF orientation value.
 *
 * No Ruby usage (internal function)
 *
 * @param bang whether the bang (!) version of the method was called
 * @param self this object
 * @return self if bang, otherwise a new image
 * @see mogrify.c (in ImageMagick 6.2.8)
 */
static VALUE
auto_orient(int bang, VALUE self)
{
    Image *image;
    VALUE new_image;
    VALUE degrees[1];

    TypedData_Get_Struct(self, Image, &rm_image_data_type, image);

    switch (image->orientation)
    {
        case TopRightOrientation:
            new_image = flipflop(bang, self, GVL_FUNC(FlopImage));
            break;

        case BottomRightOrientation:
            degrees[0] = rb_float_new(180.0);
            new_image = rotate(bang, 1, degrees, self);
            break;

        case BottomLeftOrientation:
            new_image = flipflop(bang, self, GVL_FUNC(FlipImage));
            break;

        case LeftTopOrientation:
            new_image = crisscross(bang, self, GVL_FUNC(TransposeImage));
            break;

        case RightTopOrientation:
            degrees[0] = rb_float_new(90.0);
            new_image = rotate(bang, 1, degrees, self);
            break;

        case RightBottomOrientation:
            new_image = crisscross(bang, self, GVL_FUNC(TransverseImage));
            break;

        case LeftBottomOrientation:
            degrees[0] = rb_float_new(270.0);
            new_image = rotate(bang, 1, degrees, self);
            break;

        default:                // Return IMMEDIATELY
            return bang ? Qnil : Image_copy(self);
            break;
    }


    TypedData_Get_Struct(new_image, Image, &rm_image_data_type, image);
    image->orientation = TopLeftOrientation;

    RB_GC_GUARD(new_image);

    return new_image;
}


/**
 * Rotates or flips the image based on the image's EXIF orientation tag.
 *
 * Note that only some models of modern digital cameras can tag an image with the orientation.
 * If the image does not have an orientation tag, or the image is already properly oriented, then
 * {Magick::Image#auto_orient} returns an exact copy of the image.
 *
 * @return [Magick::Image] a new image
 * @see Image#auto_orient!
 */
VALUE
Image_auto_orient(VALUE self)
{
    rm_check_destroyed(self);
    return auto_orient(False, self);
}


/**
 * Rotates or flips the image based on the image's EXIF orientation tag.
 * Note that only some models of modern digital cameras can tag an image with the orientation.
 * If the image does not have an orientation tag, or the image is already properly oriented, then
 * {Magick::Image#auto_orient!} returns nil.
 *
 * @return [Magick::Image, nil] nil if the image is already properly oriented, otherwise self
 * @see Image#auto_orient
*/
VALUE
Image_auto_orient_bang(VALUE self)
{
    rm_check_frozen(self);
    return auto_orient(True, self);
}


/**
 * Return the name of the background color as a String.
 *
 * @return [String] the background color
 */
VALUE
Image_background_color(VALUE self)
{
    Image *image = rm_check_destroyed(self);
    return rm_pixelcolor_to_color_name(image, &image->background_color);
}


/**
 * Set the the background color to the specified color spec.
 *
 * @param color [Magick::Pixel, String] the color
 * @return [Magick::Pixel, String] the given color
 */
VALUE
Image_background_color_eq(VALUE self, VALUE color)
{
    Image *image = rm_check_frozen(self);
    Color_to_PixelColor(&image->background_color, color);
    return color;
}


/**
 * Return the number of rows (before transformations).
 *
 * @return [Integer] the number of rows
 */
VALUE
Image_base_columns(VALUE self)
{
    Image *image = rm_check_destroyed(self);
    return INT2FIX(image->magick_columns);
}

/**
 * Return the image filename (before transformations).
 *
 * @return [String] the base image filename (or the current filename if there is no base)
 */
VALUE
Image_base_filename(VALUE self)
{
    Image *image = rm_check_destroyed(self);
    if (*image->magick_filename)
    {
        return rb_str_new2(image->magick_filename);
    }
    else
    {
        return rb_str_new2(image->filename);
    }
}

/**
 * Return the number of rows (before transformations).
 *
 * @return [Integer] the number of rows
 */
VALUE
Image_base_rows(VALUE self)
{
    Image *image = rm_check_destroyed(self);
    return INT2FIX(image->magick_rows);
}


/**
 * Get image bias (used when convolving an image).
 *
 * @return [Float] the image bias
 */
VALUE
Image_bias(VALUE self)
{
    Image *image;
    double bias = 0.0;

    image = rm_check_destroyed(self);
#if defined(IMAGEMAGICK_7)
    {
        const char *artifact = GetImageArtifact(image, "convolve:bias");
        if (artifact != (const char *) NULL)
        {
            char *q;

            bias = InterpretLocaleValue(artifact, &q);
            if (*q == '%')
            {
                bias *= ((double) QuantumRange + 1.0) / 100.0;
            }
        }
    }
#else
    bias = image->bias;
#endif
    return rb_float_new(bias);
}


/**
 * Set image bias (used when convolving an image).
 *
 * @param pct [Numeric, String] Either a number between 0.0 and 1.0 or a string in the form "NN%"
 * @return [Numeric, String] the given value
 */
VALUE
Image_bias_eq(VALUE self, VALUE pct)
{
    Image *image;
    double bias;

    image = rm_check_frozen(self);
    bias = rm_percentage(pct, 1.0) * QuantumRange;

#if defined(IMAGEMAGICK_7)
    {
        char artifact[21];

        snprintf(artifact, sizeof(artifact), "%.20g", bias);
        SetImageArtifact(image, "convolve:bias", artifact);
    }
#else
    image->bias = bias;
#endif

    return pct;
}

/**
 * Changes the value of individual pixels based on the intensity of each pixel channel. The result
 * is a high-contrast image.
 *
 * @overload bilevel_channel(threshold, channel = Magick::AllChannels)
 *   @param threshold [Numeric] The threshold value, a number between 0 and QuantumRange.
 *   @param channel [Magick::ChannelType] a ChannelType arguments.
 *
 * @overload bilevel_channel(threshold, *channels)
 *   @param threshold [Numeric] The threshold value, a number between 0 and QuantumRange.
 *   @param *channels [Magick::ChannelType] one or more ChannelType arguments.
 *
 * @return [Magick::Image] a new image
 */
VALUE
Image_bilevel_channel(int argc, VALUE *argv, VALUE self)
{
    Image *image, *new_image;
    ChannelType channels;
    double threshold;
#if defined(IMAGEMAGICK_7)
    ExceptionInfo *exception;
#endif

    image = rm_check_destroyed(self);
    channels = extract_channels(&argc, argv);

    if (argc > 1)
    {
        raise_ChannelType_error(argv[argc-1]);
    }
    if (argc == 0)
    {
        rb_raise(rb_eArgError, "no threshold specified");
    }

    threshold = NUM2DBL(argv[0]);
    new_image = rm_clone_image(image);

#if defined(IMAGEMAGICK_7)
    exception = AcquireExceptionInfo();
    BEGIN_CHANNEL_MASK(new_image, channels);
    GVL_STRUCT_TYPE(BilevelImage) args = { new_image, threshold, exception };
    CALL_FUNC_WITHOUT_GVL(GVL_FUNC(BilevelImage), &args);
    END_CHANNEL_MASK(new_image);
    rm_check_exception(exception, new_image, DestroyOnError);
    DestroyExceptionInfo(exception);
#else
    GVL_STRUCT_TYPE(BilevelImageChannel) args = { new_image, channels, threshold };
    CALL_FUNC_WITHOUT_GVL(GVL_FUNC(BilevelImageChannel), &args);
    rm_check_image_exception(new_image, DestroyOnError);
#endif

    return rm_image_new(new_image);
}


/**
 * Return current black point compensation attribute.
 *
 * @return [Boolean] true or false
 */
VALUE
Image_black_point_compensation(VALUE self)
{
    Image *image;
    const char *attr;
    VALUE value;

    image = rm_check_destroyed(self);

    attr = rm_get_property(image, BlackPointCompensationKey);
    if (attr && rm_strcasecmp(attr, "true") == 0)
    {
        value = Qtrue;
    }
    else
    {
        value = Qfalse;
    }

    RB_GC_GUARD(value);

    return value;
}


/**
 * Set black point compensation attribute.
 *
 * @param arg [Boolean] true or false
 * @return [Boolean] the given value
 */
VALUE
Image_black_point_compensation_eq(VALUE self, VALUE arg)
{
    Image *image;
    const char *value;

    image = rm_check_frozen(self);
    rm_set_property(image, BlackPointCompensationKey, NULL);
    value = RTEST(arg) ? "true" : "false";
    rm_set_property(image, BlackPointCompensationKey, value);

    return arg;
}


/**
 * Forces all pixels below the threshold into black while leaving all pixels above the threshold
 * unchanged.
 *
 * @overload black_threshold(red)
 *   @param red [Numeric] the number for red channel
 *
 * @overload black_threshold(red, green)
 *   @param red [Numeric] the number for red channel
 *   @param green [Numeric] the number for green channel
 *
 * @overload black_threshold(red, green, blue)
 *   @param red [Numeric] the number for red channel
 *   @param green [Numeric] the number for green channel
 *   @param blue [Numeric] the number for blue channel
 *
 * @overload black_threshold(red, green, blue, alpha:)
 *   @param red [Numeric] the number for red channel
 *   @param green [Numeric] the number for green channel
 *   @param blue [Numeric] the number for blue channel
 *   @param alpha [Numeric] the number for alpha channel
 *
 * @return [Numeric] a new image
 * @see Image#white_threshold
 */
VALUE
Image_black_threshold(int argc, VALUE *argv, VALUE self)
{
    return threshold_image(argc, argv, self, GVL_FUNC(BlackThresholdImage));
}


/**
 * Compute offsets using the gravity to determine what the offsets are relative
 * to.
 *
 * No Ruby usage (internal function)
 *
 * Notes:
 *   - No return value: modifies x_offset and y_offset directly.
 *
 * @param grav the gravity
 * @param image the destination image
 * @param mark the source image
 * @param x_offset pointer to x offset
 * @param y_offset pointer to y offset
 */
static void
get_relative_offsets(VALUE grav, Image *image, Image *mark, long *x_offset, long *y_offset)
{
    GravityType gravity;

    VALUE_TO_ENUM(grav, gravity, GravityType);

    switch (gravity)
    {
        case NorthEastGravity:
        case EastGravity:
        case SouthEastGravity:
            *x_offset = (long)(image->columns) - (long)(mark->columns) - *x_offset;
            break;
        case NorthGravity:
        case SouthGravity:
        case CenterGravity:
            *x_offset += (long)(image->columns/2) - (long)(mark->columns/2);
            break;
        default:
            break;
    }
    switch (gravity)
    {
        case SouthWestGravity:
        case SouthGravity:
        case SouthEastGravity:
            *y_offset = (long)(image->rows) - (long)(mark->rows) - *y_offset;
            break;
        case EastGravity:
        case WestGravity:
        case CenterGravity:
            *y_offset += (long)(image->rows/2) - (long)(mark->rows/2);
            break;
        case NorthEastGravity:
        case NorthGravity:
        default:
            break;
    }

}


/**
 * Compute watermark offsets from gravity type.
 *
 * No Ruby usage (internal function)
 *
 * Notes:
 *   - No return value: modifies x_offset and y_offset directly.
 *
 * @param grav the gravity
 * @param image the destination image
 * @param mark the source image
 * @param x_offset pointer to x offset
 * @param y_offset pointer to y offset
 */
static void
get_offsets_from_gravity(GravityType gravity, Image *image, Image *mark,
                         long *x_offset, long *y_offset)
{

    switch (gravity)
    {
        case ForgetGravity:
        case NorthWestGravity:
            *x_offset = 0;
            *y_offset = 0;
            break;
        case NorthGravity:
            *x_offset = ((long)(image->columns) - (long)(mark->columns)) / 2;
            *y_offset = 0;
            break;
        case NorthEastGravity:
            *x_offset = (long)(image->columns) - (long)(mark->columns);
            *y_offset = 0;
            break;
        case WestGravity:
            *x_offset = 0;
            *y_offset = ((long)(image->rows) - (long)(mark->rows)) / 2;
            break;
        case CenterGravity:
        default:
            *x_offset = ((long)(image->columns) - (long)(mark->columns)) / 2;
            *y_offset = ((long)(image->rows) - (long)(mark->rows)) / 2;
            break;
        case EastGravity:
            *x_offset = (long)(image->columns) - (long)(mark->columns);
            *y_offset = ((long)(image->rows) - (long)(mark->rows)) / 2;
            break;
        case SouthWestGravity:
            *x_offset = 0;
            *y_offset = (long)(image->rows) - (long)(mark->rows);
            break;
        case SouthGravity:
            *x_offset = ((long)(image->columns) - (long)(mark->columns)) / 2;
            *y_offset = (long)(image->rows) - (long)(mark->rows);
            break;
        case SouthEastGravity:
            *x_offset = (long)(image->columns) - (long)(mark->columns);
            *y_offset = (long)(image->rows) - (long)(mark->rows);
            break;
    }
}


/**
 * Called from rb_protect, returns the number if obj is really a numeric value.
 *
 * No Ruby usage (internal function)
 *
 * @param obj the value
 * @return numeric value of obj
 * @todo Make sure that we are really returning the obj here
 */
static VALUE
check_for_long_value(VALUE obj)
{
    return LONG2NUM(NUM2LONG(obj));
}


/**
 * Compute x- and y-offset of source image for a compositing method.
 *
 * No Ruby usage (internal function)
 *
 * Notes:
 *   - No return value: modifies x_offset and y_offset directly.
 *
 * @param argc number of input arguments
 * @param argv array of input arguments
 * @param dest the destination image
 * @param src the source image
 * @param x_offset pointer to x offset
 * @param y_offset pointer to y offset
 */
static void
get_composite_offsets(int argc, VALUE *argv, Image *dest, Image *src,
                      long *x_offset, long *y_offset)
{
    GravityType gravity;
    int exc = 0;

    if (CLASS_OF(argv[0]) == Class_GravityType)
    {
        VALUE_TO_ENUM(argv[0], gravity, GravityType);

        switch (argc)
        {
            // Gravity + offset(s). Offsets are relative to the image edges
            // as specified by the gravity.
            case 3:
                *y_offset = NUM2LONG(argv[2]);
            case 2:
                *x_offset = NUM2LONG(argv[1]);
                get_relative_offsets(argv[0], dest, src, x_offset, y_offset);
                break;
            case 1:
                // No offsets specified. Compute offset based on the gravity alone.
                get_offsets_from_gravity(gravity, dest, src, x_offset, y_offset);
                break;
        }
    }
    // Gravity not specified at all. Offsets are measured from the
    // NorthWest corner. The arguments must be numbers.
    else
    {
        rb_protect(check_for_long_value, argv[0], &exc);
        if (exc)
        {
            rb_raise(rb_eTypeError, "expected GravityType, got %s",
                     rb_class2name(CLASS_OF(argv[0])));
        }
        *x_offset = NUM2LONG(argv[0]);
        if (argc > 1)
        {
            *y_offset = NUM2LONG(argv[1]);
        }
    }

}


/**
 * Convert 2 doubles to a blend or dissolve geometry string.
 *
 * No Ruby usage (internal function)
 *
 * Notes:
 *   - the geometry buffer needs to be at least 16 characters long.
 *   - For safety's sake this function asserts that it is at least 20 characters
 *     long.
 *   - The percentages must be in the range -1000 < n < 1000. This is far in
 *     excess of what xMagick will allow.
 *
 * @param geometry the geometry
 * @param geometry_l length of geometry
 * @param src_percent source percentage
 * @param dst_percent destination percentage
 */
static void
blend_geometry(char *geometry, size_t geometry_l, double src_percent, double dst_percent)
{
    size_t sz = 0;
    int fw, prec;

    if (fabs(src_percent) >= 1000.0 || fabs(dst_percent) >= 1000.0)
    {
        if (fabs(src_percent) < 1000.0)
        {
            src_percent = dst_percent;
        }
        rb_raise(rb_eArgError, "%g is out of range +/-999.99", src_percent);
    }

    assert(geometry_l >= 20);
    memset(geometry, 0xdf, geometry_l);

    fw = 4;
    prec = 0;
    if (src_percent != floor(src_percent))
    {
        prec = 2;
        fw += 3;
    }

    sz = (size_t)snprintf(geometry, geometry_l, "%*.*f", -fw, prec, src_percent);
    assert(sz < geometry_l);

    sz = strcspn(geometry, " ");

    // if dst_percent was nil don't add to the geometry
    if (dst_percent != -1.0)
    {
        fw = 4;
        prec = 0;
        if (dst_percent != floor(dst_percent))
        {
            prec = 2;
            fw += 3;
        }


        sz += (size_t)snprintf(geometry+sz, geometry_l-sz, "x%*.*f", -fw, prec, dst_percent);
        assert(sz < geometry_l);
        sz = strcspn(geometry, " ");
    }

    if (sz < geometry_l)
    {
        memset(geometry+sz, 0x00, geometry_l-sz);
    }

}


/**
 * Create a composite of an image and an overlay (for blending, dissolving, etc.).
 *
 * No Ruby usage (internal function)
 *
 * @param image the original image
 * @param overlay the overlay
 * @param image_pct image percentage
 * @param overlay_pct overlay percentage
 * @param x_off the x offset
 * @param y_off the y offset
 * @param op the composite operator to use
 * @return a new image
 */
static VALUE
special_composite(Image *image, Image *overlay, double image_pct, double overlay_pct,
                  long x_off, long y_off, CompositeOperator op)
{
    Image *new_image;
    char geometry[20];
#if defined(IMAGEMAGICK_7)
    ExceptionInfo *exception;
#endif

    blend_geometry(geometry, sizeof(geometry), image_pct, overlay_pct);
    CloneString(&overlay->geometry, geometry);
    SetImageArtifact(overlay, "compose:args", geometry);

    new_image = rm_clone_image(image);
    SetImageArtifact(new_image, "compose:args", geometry); // 6.9 appears to get this info from canvas (dest) image


#if defined(IMAGEMAGICK_7)
    exception = AcquireExceptionInfo();
    GVL_STRUCT_TYPE(CompositeImage) args = { new_image, overlay, op, MagickTrue, x_off, y_off, exception };
    CALL_FUNC_WITHOUT_GVL(GVL_FUNC(CompositeImage), &args);
    rm_check_exception(exception, new_image, DestroyOnError);
    DestroyExceptionInfo(exception);
#else
    GVL_STRUCT_TYPE(CompositeImage) args = { new_image, op, overlay, x_off, y_off };
    CALL_FUNC_WITHOUT_GVL(GVL_FUNC(CompositeImage), &args);

    rm_check_image_exception(new_image, DestroyOnError);
#endif

    return rm_image_new(new_image);
}


/**
 * Adds the overlay image to the target image according to src_percent and dst_percent.
 *
 * - The default value for dst_percent is 100%-src_percent
 *
 * @overload blend(overlay, src_percent, dst_percent, gravity = Magick::NorthWestGravity, x_offset = 0, y_offset = 0)
 *   @param overlay [Magick::Image, Magick::ImageList] The source image for the composite operation.
 *     Either an imagelist or an image. If an imagelist, uses the current image.
 *   @param src_percent [Numeric, String] Either a non-negative number a string in the form "NN%".
 *     If src_percentage is a number it is interpreted as a percentage.
 *     Both 0.25 and "25%" mean 25%. This argument is required.
 *   @param dst_percent [Numeric, String] Either a non-negative number a string in the form "NN%".
 *     If src_percentage is a number it is interpreted as a percentage.
 *     Both 0.25 and "25%" mean 25%. This argument may omitted if no other arguments follow it.
 *     In this case the default is 100%-src_percentage.
 *   @param gravity [Magick::GravityType] the gravity for offset. the offsets are measured from the NorthWest corner by default.
 *   @param x_offset [Numeric] The offset that measured from the left-hand side of the target image.
 *   @param y_offset [Numeric] The offset that measured from the top of the target image.
 *   @return [Magick::Image] a new image
 */
VALUE
Image_blend(int argc, VALUE *argv, VALUE self)
{
    VALUE ovly;
    Image *image, *overlay;
    double src_percent, dst_percent;
    long x_offset = 0L, y_offset = 0L;

    image = rm_check_destroyed(self);

    if (argc < 1)
    {
        rb_raise(rb_eArgError, "wrong number of arguments (%d for 2 to 6)", argc);
    }

    ovly = rm_cur_image(argv[0]);
    overlay = rm_check_destroyed(ovly);

    if (argc > 3)
    {
        get_composite_offsets(argc-3, &argv[3], image, overlay, &x_offset, &y_offset);
        // There must be 3 arguments left
        argc = 3;
    }

    switch (argc)
    {
        case 3:
            dst_percent = rm_percentage(argv[2], 1.0) * 100.0;
            src_percent = rm_percentage(argv[1], 1.0) * 100.0;
            break;
        case 2:
            src_percent = rm_percentage(argv[1], 1.0) * 100.0;
            dst_percent = FMAX(100.0 - src_percent, 0);
            break;
        default:
            rb_raise(rb_eArgError, "wrong number of arguments (%d for 2 to 6)", argc);
            break;
    }

    RB_GC_GUARD(ovly);

    return special_composite(image, overlay, src_percent, dst_percent,
                             x_offset, y_offset, BlendCompositeOp);

}



/**
 * Simulate a scene at nighttime in the moonlight.
 *
 * @overload blue_shift(factor = 1.5)
 *   @param factor [Numeric] Larger values increase the effect.
 *   @return [Magick::Image] a new image
 */
VALUE
Image_blue_shift(int argc, VALUE *argv, VALUE self)
{
    Image *image, *new_image;
    double factor = 1.5;
    ExceptionInfo *exception;

    image = rm_check_destroyed(self);

    switch (argc)
    {
        case 1:
            factor = NUM2DBL(argv[0]);
        case 0:
            break;
        default:
            rb_raise(rb_eArgError, "wrong number of arguments (%d for 0 or 1)", argc);
            break;
    }


    exception = AcquireExceptionInfo();
    GVL_STRUCT_TYPE(BlueShiftImage) args = { image, factor, exception };
    new_image = (Image *)CALL_FUNC_WITHOUT_GVL(GVL_FUNC(BlueShiftImage), &args);
    CHECK_EXCEPTION();
    DestroyExceptionInfo(exception);

    return rm_image_new(new_image);
}


/**
 * Blurs the specified channel.
 * Convolves the image with a Gaussian operator of the given radius and standard deviation (sigma).
 *
 * @overload blur_channel(radius = 0.0, sigma = 1.0, channel = Magick::AllChannels)
 *   @param radius [Numeric] the radius value
 *   @param sigma [Numeric] the sigma value
 *   @param channel [Magick::ChannelType] a ChannelType arguments.
 *
 * @overload blur_channel(radius = 0.0, sigma = 1.0, *channels)
 *   @param radius [Numeric] the radius value
 *   @param sigma [Numeric] the sigma value
 *   @param *channels [Magick::ChannelType] one or more ChannelType arguments.
 *
 * @return [Magick::Image] a new image
 */
VALUE
Image_blur_channel(int argc, VALUE *argv, VALUE self)
{
    Image *image, *new_image;
    ExceptionInfo *exception;
    ChannelType channels;
    double radius = 0.0, sigma = 1.0;

    image = rm_check_destroyed(self);

    channels = extract_channels(&argc, argv);

    // There can be 0, 1, or 2 remaining arguments.
    switch (argc)
    {
        case 2:
            sigma = NUM2DBL(argv[1]);
        case 1:
            radius = NUM2DBL(argv[0]);
        case 0:
            break;
        default:
            raise_ChannelType_error(argv[argc-1]);
    }

    exception = AcquireExceptionInfo();
#if defined(IMAGEMAGICK_7)
    BEGIN_CHANNEL_MASK(image, channels);
    GVL_STRUCT_TYPE(BlurImage) args = { image, radius, sigma, exception };
    new_image = (Image *)CALL_FUNC_WITHOUT_GVL(GVL_FUNC(BlurImage), &args);
    CHANGE_RESULT_CHANNEL_MASK(new_image);
    END_CHANNEL_MASK(image);
#else
    GVL_STRUCT_TYPE(BlurImageChannel) args = { image, channels, radius, sigma, exception };
    new_image = (Image *)CALL_FUNC_WITHOUT_GVL(GVL_FUNC(BlurImageChannel), &args);
#endif
    rm_check_exception(exception, new_image, DestroyOnError);
    DestroyExceptionInfo(exception);

    return rm_image_new(new_image);
}


/**
 * Blur the image.
 *
 * @overload blur_image(radius = 0.0, sigma = 1.0)
 *   @param radius [Numeric] the radius value
 *   @param sigma [Numeric] the sigma value
 *   @return [Magick::Image] a new image
 */
VALUE
Image_blur_image(int argc, VALUE *argv, VALUE self)
{
    return effect_image(self, argc, argv, GVL_FUNC(BlurImage));
}


/**
 * Surrounds the image with a border of the specified width, height, and named
 * color.
 *
 * No Ruby usage (internal function)
 *
 * @param bang whether the bang (!) version of the method was called
 * @param self this object
 * @param width the width of the border
 * @param height the height of the border
 * @param color the color of the border
 * @return self if bang, otherwise a new image
 * @see Image_border
 * @see Image_border_bang
 */
static VALUE
border(int bang, VALUE self, VALUE width, VALUE height, VALUE color)
{
    Image *image, *new_image;
    PixelColor old_border;
    ExceptionInfo *exception;
    RectangleInfo rect;

    TypedData_Get_Struct(self, Image, &rm_image_data_type, image);

    memset(&rect, 0, sizeof(rect));
    rect.width = NUM2UINT(width);
    rect.height = NUM2UINT(height);

    // Save current border color - we'll want to restore it afterwards.
    old_border = image->border_color;
    Color_to_PixelColor(&image->border_color, color);

    exception = AcquireExceptionInfo();
#if defined(IMAGEMAGICK_7)
    GVL_STRUCT_TYPE(BorderImage) args = { image, &rect, image->compose, exception };
#else
    GVL_STRUCT_TYPE(BorderImage) args = { image, &rect, exception };
#endif
    new_image = (Image *)CALL_FUNC_WITHOUT_GVL(GVL_FUNC(BorderImage), &args);
    rm_check_exception(exception, new_image, DestroyOnError);
    DestroyExceptionInfo(exception);

    if (bang)
    {
        rm_ensure_result(new_image);
        new_image->border_color = old_border;
        UPDATE_DATA_PTR(self, new_image);
        rm_image_destroy(image);
        return self;
    }

    image->border_color = old_border;
    return rm_image_new(new_image);
}

/**
 * Surrounds the image with a border of the specified width, height, and named color.
 * In-place form of {Image#border}.
 *
 * @param width [Numeric] the width of the border
 * @param height [Numeric] the height of the border
 * @param color [Magick::Pixel, String] the color of the border
 */
VALUE
Image_border_bang(VALUE self, VALUE width, VALUE height, VALUE color)
{
    rm_check_frozen(self);
    return border(True, self, width, height, color);
}


/**
 * Surrounds the image with a border of the specified width, height, and named color.
 *
 * @param width [Numeric] the width of the border
 * @param height [Numeric] the height of the border
 * @param color [Magick::Pixel, String] the color of the border
 * @return [Magick::Image] a new image
 */
VALUE
Image_border(VALUE self, VALUE width, VALUE height, VALUE color)
{
    rm_check_destroyed(self);
    return border(False, self, width, height, color);
}


/**
 * Return the name of the border color as a String.
 *
 * @return [String] the name of the border color
 */
VALUE
Image_border_color(VALUE self)
{
    Image *image = rm_check_destroyed(self);
    return rm_pixelcolor_to_color_name(image, &image->border_color);
}


/**
 * Set the the border color.
 *
 * @param [Magick::Pixel, String] color the color
 * @return [Magick::Pixel, String] the given color
 */
VALUE
Image_border_color_eq(VALUE self, VALUE color)
{
    Image *image = rm_check_frozen(self);
    Color_to_PixelColor(&image->border_color, color);
    return color;
}


/**
 * Returns the bounding box of an image canvas.
 *
 * @return [Magick::Rectangle] the bounding box
 */
VALUE
Image_bounding_box(VALUE self)
{
    Image *image;
    RectangleInfo box;
    ExceptionInfo *exception;

    image = rm_check_destroyed(self);
    exception = AcquireExceptionInfo();
    box = GetImageBoundingBox(image, exception);
    CHECK_EXCEPTION();

    DestroyExceptionInfo(exception);

    return Import_RectangleInfo(&box);
}


/**
 * Reads an image from an X window.
 * Unless you identify a window to capture via the optional arguments block, when capture is invoked
 * the cursor will turn into a cross. Click the cursor on the window to be captured.
 *
 * @overload capture(silent = false, frame = false, descend = false, screen = false, borders = false)
 *   @param silent [Boolean] If true, suppress the beeps that signal the start and finish of the
 *     capture process.
 *   @param frame [Boolean] If true, include the window frame.
 *   @param descend [Boolean] If true, obtain image by descending window hierarchy.
 *   @param screen [Boolean] If true, specifies that the GetImage request used to obtain the image
 *     should be done on the root window, rather than directly on the specified window. In this way,
 *     you can obtain pieces of other windows that overlap the specified window, and more
 *     importantly, you can capture menus or other popups that are independent windows but appear
 *     over the specified window.
 *   @param borders [Boolean] If true, include the border in the image.
 *
 * @overload capture(silent = false, frame = false, descend = false, screen = false, borders = false)
 *   This yields {Magick::Image::Info} to block with its object's scope.
 *   @param silent [Boolean] If true, suppress the beeps that signal the start and finish of the
 *     capture process.
 *   @param frame [Boolean] If true, include the window frame.
 *   @param descend [Boolean] If true, obtain image by descending window hierarchy.
 *   @param screen [Boolean] If true, specifies that the GetImage request used to obtain the image
 *     should be done on the root window, rather than directly on the specified window. In this way,
 *     you can obtain pieces of other windows that overlap the specified window, and more
 *     importantly, you can capture menus or other popups that are independent windows but appear
 *     over the specified window.
 *   @param borders [Boolean] If true, include the border in the image.
 *   @yield [info]
 *   @yieldparam info [Magick::Image::Info]
 *
 * @return [Magick::Image] a new image
 * @example
 *   img = Image.capture { |options|
 *     options.filename = "root"
 *   }
 */
VALUE
Image_capture(int argc, VALUE *argv, VALUE self ATTRIBUTE_UNUSED)
{
    Image *new_image;
    ImageInfo *image_info;
    VALUE info_obj;
    XImportInfo ximage_info;
#if defined(IMAGEMAGICK_7)
    ExceptionInfo *exception;
#endif

    XGetImportInfo(&ximage_info);
    switch (argc)
    {
        case 5:
            ximage_info.borders = (MagickBooleanType)RTEST(argv[4]);
        case 4:
            ximage_info.screen  = (MagickBooleanType)RTEST(argv[3]);
        case 3:
            ximage_info.descend = (MagickBooleanType)RTEST(argv[2]);
        case 2:
            ximage_info.frame   = (MagickBooleanType)RTEST(argv[1]);
        case 1:
            ximage_info.silent  = (MagickBooleanType)RTEST(argv[0]);
        case 0:
            break;
        default:
            rb_raise(rb_eArgError, "wrong number of arguments (%d for 0 to 5)", argc);
            break;
    }

    // Get optional parms.
    // Set info->filename = "root", window ID number or window name,
    //  or nothing to do an interactive capture
    // Set info->server_name to the server name
    // Also info->colorspace, depth, dither, interlace, type
    info_obj = rm_info_new();
    TypedData_Get_Struct(info_obj, Info, &rm_info_data_type, image_info);

    // If an error occurs, IM will call our error handler and we raise an exception.
#if defined(IMAGEMAGICK_7)
    exception = AcquireExceptionInfo();
    new_image = XImportImage(image_info, &ximage_info, exception);
    rm_check_exception(exception, new_image, DestroyOnError);
    DestroyExceptionInfo(exception);
#else
    new_image = XImportImage(image_info, &ximage_info);
    rm_check_image_exception(new_image, DestroyOnError);
#endif

    rm_ensure_result(new_image);

    rm_set_user_artifact(new_image, image_info);
    rm_sync_image_options(new_image, image_info);

    RB_GC_GUARD(info_obj);

    return rm_image_new(new_image);
}


/**
 * This method supports resizing a method by specifying constraints.
 * For example, you can specify that the image should be resized such that the aspect ratio should
 * be retained but the resulting image should be no larger than 640 pixels wide and 480 pixels tall.
 *
 * @param geom_arg [Magick::Geometry, String] the geometry
 * @yield [column, row, image]
 * @yieldparam column [Integer] The desired column size
 * @yieldparam row [Integer] The desired row size
 * @yieldparam image [Magick::Image] self
 * @see https://www.imagemagick.org/Magick++/Geometry.html
 * @example
 *   image.change_geometry!('320x240') { |cols, rows, img|
 *     img.resize!(cols, rows)
 *   }
 * @note {Magick::Image#change_geometry!} is an alias for {Magick::Image#change_geometry}.
 */
VALUE
Image_change_geometry(VALUE self, VALUE geom_arg)
{
    Image *image;
    RectangleInfo rect;
    VALUE geom_str;
    char *geometry;
    unsigned int flags;
    VALUE ary;

    image = rm_check_destroyed(self);
    geom_str = rb_String(geom_arg);
    geometry = StringValueCStr(geom_str);

    memset(&rect, 0, sizeof(rect));

    SetGeometry(image, &rect);
    flags = ParseMetaGeometry(geometry, &rect.x, &rect.y, &rect.width, &rect.height);
    if (flags == NoValue)
    {
        rb_raise(rb_eArgError, "invalid geometry string `%s'", geometry);
    }

    ary = rb_ary_new2(3);
    rb_ary_store(ary, 0, ULONG2NUM(rect.width));
    rb_ary_store(ary, 1, ULONG2NUM(rect.height));
    rb_ary_store(ary, 2, self);

    RB_GC_GUARD(geom_str);
    RB_GC_GUARD(ary);

    return rb_yield(ary);
}


/**
 * Return true if any pixel in the image has been altered since the image was constituted.
 *
 * @return [Boolean] true if altered, false otherwise
 */
VALUE
Image_changed_q(VALUE self)
{
    Image *image = rm_check_destroyed(self);
    VALUE okay = IsTaintImage(image) ? Qtrue : Qfalse;
    return okay;
}


/**
 * Extract a channel from the image. A channel is a particular color component of each pixel in the
 * image.
 *
 * @param channel_arg [Magick::ChannelType] the type of the channel to extract
 * @return [Magick::Image] a new image
 */
VALUE
Image_channel(VALUE self, VALUE channel_arg)
{
    Image *image, *new_image;
    ChannelType channel;
#if defined(IMAGEMAGICK_7)
    ExceptionInfo *exception;
#endif

    image = rm_check_destroyed(self);

    VALUE_TO_ENUM(channel_arg, channel, ChannelType);

#if defined(IMAGEMAGICK_7)
    exception = AcquireExceptionInfo();
    GVL_STRUCT_TYPE(SeparateImage) args = { image, channel, exception };
    new_image = (Image *)CALL_FUNC_WITHOUT_GVL(GVL_FUNC(SeparateImage), &args);
    rm_check_exception(exception, new_image, DestroyOnError);
    DestroyExceptionInfo(exception);
#else
    new_image = rm_clone_image(image);
    GVL_STRUCT_TYPE(SeparateImageChannel) args = { new_image, channel };
    CALL_FUNC_WITHOUT_GVL(GVL_FUNC(SeparateImageChannel), &args);

    rm_check_image_exception(new_image, DestroyOnError);
#endif

    return rm_image_new(new_image);
}


/**
 * Returns the maximum depth for the specified channel or channels.
 *
 * @overload channel_depth(channel = Magick::AllChannels)
 *   @param channel [Magick::ChannelType] a ChannelType arguments.
 *
 * @overload channel_depth(*channels)
 *   @param *channels [Magick::ChannelType] one or more ChannelType arguments.
 *
 * @return [Integer] the channel depth
 */
VALUE
Image_channel_depth(int argc, VALUE *argv, VALUE self)
{
    Image *image;
    ChannelType channels;
    size_t channel_depth;
    ExceptionInfo *exception;

    image = rm_check_destroyed(self);
    channels = extract_channels(&argc, argv);

    // Ensure all arguments consumed.
    if (argc > 0)
    {
        raise_ChannelType_error(argv[argc-1]);
    }

    exception = AcquireExceptionInfo();

#if defined(IMAGEMAGICK_7)
    BEGIN_CHANNEL_MASK(image, channels);
    GVL_STRUCT_TYPE(GetImageDepth) args = { image, exception };
    channel_depth = (size_t)CALL_FUNC_WITHOUT_GVL(GVL_FUNC(GetImageDepth), &args);
    END_CHANNEL_MASK(image);
#else
    GVL_STRUCT_TYPE(GetImageChannelDepth) args = { image, channels, exception };
    channel_depth = (size_t)CALL_FUNC_WITHOUT_GVL(GVL_FUNC(GetImageChannelDepth), &args);
#endif
    CHECK_EXCEPTION();

    DestroyExceptionInfo(exception);

    return ULONG2NUM(channel_depth);
}


/**
 * Returns the minimum and maximum intensity values for the specified channel or channels.
 *
 * @overload channel_extrema(channel = Magick::AllChannels)
 *   @param channel [Magick::ChannelType] a ChannelType arguments.
 *
 * @overload channel_extrema(*channels)
 *   @param *channels [Magick::ChannelType] one or more ChannelType arguments.
 *
 * @return [Array<Integer>] The first element in the array is the minimum value. The second element is the
 *   maximum value.
 */
VALUE
Image_channel_extrema(int argc, VALUE *argv, VALUE self)
{
    Image *image;
    ChannelType channels;
    ExceptionInfo *exception;
    size_t min, max;
    VALUE ary;

    image = rm_check_destroyed(self);

    channels = extract_channels(&argc, argv);

    // Ensure all arguments consumed.
    if (argc > 0)
    {
        raise_ChannelType_error(argv[argc-1]);
    }

    exception = AcquireExceptionInfo();
#if defined(IMAGEMAGICK_7)
    BEGIN_CHANNEL_MASK(image, channels);
    GVL_STRUCT_TYPE(GetImageExtrema) args = { image, &min, &max, exception };
    CALL_FUNC_WITHOUT_GVL(GVL_FUNC(GetImageExtrema), &args);
    END_CHANNEL_MASK(image);
#else
    GVL_STRUCT_TYPE(GetImageChannelExtrema) args = { image, channels, &min, &max, exception };
    CALL_FUNC_WITHOUT_GVL(GVL_FUNC(GetImageChannelExtrema), &args);
#endif
    CHECK_EXCEPTION();

    DestroyExceptionInfo(exception);

    ary = rb_ary_new2(2);
    rb_ary_store(ary, 0, ULONG2NUM(min));
    rb_ary_store(ary, 1, ULONG2NUM(max));

    RB_GC_GUARD(ary);

    return ary;
}


/**
 * Returns the mean and standard deviation values for the specified channel or channels.
 *
 * @overload channel_mean(channel = Magick::AllChannels)
 *   @param channel [Magick::ChannelType] a ChannelType arguments.
 *
 * @overload channel_mean(*channels)
 *   @param *channels [Magick::ChannelType] one or more ChannelType arguments.
 *
 * @return [Array<Float>] The first element in the array is the mean value. The second element is the
 *   standard deviation.
 */
VALUE
Image_channel_mean(int argc, VALUE *argv, VALUE self)
{
    Image *image;
    ChannelType channels;
    ExceptionInfo *exception;
    double mean, stddev;
    VALUE ary;

    image = rm_check_destroyed(self);

    channels = extract_channels(&argc, argv);

    // Ensure all arguments consumed.
    if (argc > 0)
    {
        raise_ChannelType_error(argv[argc-1]);
    }

    exception = AcquireExceptionInfo();
#if defined(IMAGEMAGICK_7)
    BEGIN_CHANNEL_MASK(image, channels);
    GVL_STRUCT_TYPE(GetImageMean) args = { image, &mean, &stddev, exception };
    CALL_FUNC_WITHOUT_GVL(GVL_FUNC(GetImageMean), &args);
    END_CHANNEL_MASK(image);
#else
    GVL_STRUCT_TYPE(GetImageChannelMean) args = { image, channels, &mean, &stddev, exception };
    CALL_FUNC_WITHOUT_GVL(GVL_FUNC(GetImageChannelMean), &args);
#endif
    CHECK_EXCEPTION();

    DestroyExceptionInfo(exception);

    ary = rb_ary_new2(2);
    rb_ary_store(ary, 0, rb_float_new(mean));
    rb_ary_store(ary, 1, rb_float_new(stddev));

    RB_GC_GUARD(ary);

    return ary;
}

/**
 * Return an array of the entropy for the channel.
 *
 * @overload channel_entropy(channel = Magick::AllChannels)
 *   @param channel [Magick::ChannelType] a ChannelType arguments.
 *
 * @overload channel_entropy(*channels)
 *   @param *channels [Magick::ChannelType] one or more ChannelType arguments.
 *
 * @return [Array<Float>] The first element in the array is the average entropy of the selected channels.
 */
VALUE
Image_channel_entropy(int argc, VALUE *argv, VALUE self)
{
    Image *image;
    ChannelType channels;
    ExceptionInfo *exception;
    double entropy;
    VALUE ary;

    image = rm_check_destroyed(self);

    channels = extract_channels(&argc, argv);

    // Ensure all arguments consumed.
    if (argc > 0)
    {
        raise_ChannelType_error(argv[argc-1]);
    }

    exception = AcquireExceptionInfo();
#if defined(IMAGEMAGICK_7)
    BEGIN_CHANNEL_MASK(image, channels);
    GVL_STRUCT_TYPE(GetImageEntropy) args = { image, &entropy, exception };
    CALL_FUNC_WITHOUT_GVL(GVL_FUNC(GetImageEntropy), &args);
    END_CHANNEL_MASK(image);
#else
    GVL_STRUCT_TYPE(GetImageChannelEntropy) args = { image, channels, &entropy, exception };
    CALL_FUNC_WITHOUT_GVL(GVL_FUNC(GetImageChannelEntropy), &args);
#endif
    CHECK_EXCEPTION();

    DestroyExceptionInfo(exception);

    ary = rb_ary_new2(1);
    rb_ary_store(ary, 0, rb_float_new(entropy));

    RB_GC_GUARD(ary);

    return ary;
}


/**
 * Return a new image that is a copy of the input image with the edges highlighted.
 *
 * @overload charcoal(radius = 0.0, sigma = 1.0)
 *   @param radius [Numeric] The radius of the pixel neighborhood.
 *   @param sigma [Numeric] The standard deviation of the Gaussian, in pixels.
 *   @return [Magick::Image] a new image
 */
VALUE
Image_charcoal(int argc, VALUE *argv, VALUE self)
{
    return effect_image(self, argc, argv, GVL_FUNC(CharcoalImage));
}


/**
 * Raises {Magick::DestroyedImageError} if the image has been destroyed. Returns nil otherwise.
 *
 * @return [nil] nil
 * @raise [Magick::DestroyedImageError] raise if the image has been destroyed
 */
VALUE
Image_check_destroyed(VALUE self)
{
    rm_check_destroyed(self);
    return Qnil;
}


/**
 * Remove a region of an image and collapses the image to occupy the removed portion.
 *
 * @param x [Numeric] x position of start of region
 * @param y [Numeric] y position of start of region
 * @param width [Numeric] width of region
 * @param height [Numeric] height of region
 * @return [Magick::Image] a new image
 */
VALUE
Image_chop(VALUE self, VALUE x, VALUE y, VALUE width, VALUE height)
{
    rm_check_destroyed(self);
    return xform_image(False, self, x, y, width, height, GVL_FUNC(ChopImage));
}


/**
 * Return the red, green, blue, and white-point chromaticity values as a {Magick::Chromaticity}.
 *
 * @return [Magick::Chromaticity] the chromaticity values
 */
VALUE
Image_chromaticity(VALUE self)
{
    Image *image = rm_check_destroyed(self);
    return ChromaticityInfo_new(&image->chromaticity);
}


/**
 * Set the red, green, blue, and white-point chromaticity values from a {Magick::Chromaticity}.
 *
 * @param [Magick::Chromaticity] chroma the chromaticity
 * @return [Magick::Chromaticity] the given value
 */
VALUE
Image_chromaticity_eq(VALUE self, VALUE chroma)
{
    Image *image = rm_check_frozen(self);
    Export_ChromaticityInfo(&image->chromaticity, chroma);
    return chroma;
}


/**
 * Same as {Magick::Image#dup} except the frozen state of the original is propagated to the new
 * copy.
 *
 * @return [Magick::Image] a clone of this object
 */
VALUE
Image_clone(VALUE self)
{
    VALUE clone;

    clone = Image_dup(self);
    if (OBJ_FROZEN(self))
    {
        OBJ_FREEZE(clone);
    }

    RB_GC_GUARD(clone);

    return clone;
}


/**
 * Replace the channel values in the target image with a lookup of its replacement value in an LUT
 * gradient image.
 *
 * The LUT image should be either a single row or column image of replacement colors.
 * The lookup is controlled by the -interpolate setting, especially for an LUT which is not the full
 * length needed by the IM installed Quality (Q) level.  Good settings for this is the default
 * 'bilinear' or 'bicubic' interpolation setting for a smooth color gradient, or 'integer' for a
 * direct unsmoothed lookup of color values.
 *
 * This method is especially suited to replacing a grayscale image with specific color gradient from
 * the CLUT image.
 *
 * @overload clut_channel(clut_image, channel = Magick::AllChannels)
 *   @param clut_image [Magick::Image, Magick::ImageList] The LUT gradient image.
 *   @param channel [Magick::ChannelType] a ChannelType arguments.
 *
 * @overload clut_channel(clut_image, *channels)
 *   @param clut_image [Magick::Image, Magick::ImageList] The LUT gradient image.
 *   @param *channels [Magick::ChannelType] one or more ChannelType arguments.
 *
 * @return [Magick::Image] self
 */
VALUE
Image_clut_channel(int argc, VALUE *argv, VALUE self)
{
    Image *image, *clut;
    ChannelType channels;
    MagickBooleanType okay;
#if defined(IMAGEMAGICK_7)
    ExceptionInfo *exception;
#endif

    image = rm_check_frozen(self);

    // check_destroyed before confirming the arguments
    if (argc >= 1)
    {
        clut = rm_check_destroyed(rm_cur_image(argv[0]));
        channels = extract_channels(&argc, argv);
        if (argc != 1)
        {
            rb_raise(rb_eArgError, "wrong number of arguments (%d for 1 or more)", argc);
        }
    }
    else
    {
        rb_raise(rb_eArgError, "wrong number of arguments (%d for 1 or more)", argc);
    }

#if defined(IMAGEMAGICK_7)
    exception = AcquireExceptionInfo();
    BEGIN_CHANNEL_MASK(image, channels);
    GVL_STRUCT_TYPE(ClutImage) args = { image, clut, image->interpolate, exception };
    void *ret = CALL_FUNC_WITHOUT_GVL(GVL_FUNC(ClutImage), &args);
    okay = static_cast<MagickBooleanType>(reinterpret_cast<intptr_t &>(ret));
    END_CHANNEL_MASK(image);
    CHECK_EXCEPTION();
    DestroyExceptionInfo(exception);
#else
    GVL_STRUCT_TYPE(ClutImageChannel) args = { image, channels, clut };
    void *ret = CALL_FUNC_WITHOUT_GVL(GVL_FUNC(ClutImageChannel), &args);
    okay = static_cast<MagickBooleanType>(reinterpret_cast<intptr_t &>(ret));
    rm_check_image_exception(image, RetainOnError);
    rm_check_image_exception(clut, RetainOnError);
#endif
    if (!okay)
    {
        rb_raise(rb_eRuntimeError, "ClutImageChannel failed.");
    }

    return self;
}


/**
 * Computes the number of times each unique color appears in the image.
 *
 * @return [Hash<Magick::Pixel, Integer>] Each key in the hash is a pixel representing a color that appears in the image.
 *   The value associated with the key is the number of times that color appears in the image.
 */
VALUE
Image_color_histogram(VALUE self)
{
    Image *image, *dc_copy = NULL;
    VALUE hash, pixel;
    size_t x, colors;
    ExceptionInfo *exception;
#if defined(IMAGEMAGICK_7)
    PixelInfo *histogram;
#else
    ColorPacket *histogram;
#endif

    image = rm_check_destroyed(self);

    exception = AcquireExceptionInfo();

    // If image not DirectClass make a DirectClass copy.
    if (image->storage_class != DirectClass)
    {
        dc_copy = rm_clone_image(image);
#if defined(IMAGEMAGICK_7)
        GVL_STRUCT_TYPE(SetImageStorageClass) args = { dc_copy, DirectClass, exception };
#else
        GVL_STRUCT_TYPE(SetImageStorageClass) args = { dc_copy, DirectClass };
#endif
        CALL_FUNC_WITHOUT_GVL(GVL_FUNC(SetImageStorageClass), &args);
        image = dc_copy;
    }

    GVL_STRUCT_TYPE(GetImageHistogram) args = { image, &colors, exception };
    void *ret = CALL_FUNC_WITHOUT_GVL(GVL_FUNC(GetImageHistogram), &args);
    histogram = reinterpret_cast<decltype(histogram)>(ret);

    if (histogram == NULL)
    {
        if (dc_copy)
        {
            DestroyImage(dc_copy);
        }
        rb_raise(rb_eNoMemError, "not enough memory to continue");
    }
    if (rm_should_raise_exception(exception, DestroyExceptionRetention))
    {
        RelinquishMagickMemory(histogram);
        if (dc_copy)
        {
            DestroyImage(dc_copy);
        }

        rm_raise_exception(exception);
    }

    hash = rb_hash_new();
    for (x = 0; x < colors; x++)
    {
#if defined(IMAGEMAGICK_7)
        pixel = Pixel_from_PixelColor(&histogram[x]);
#else
        pixel = Pixel_from_PixelColor(&histogram[x].pixel);
#endif
        rb_hash_aset(hash, pixel, ULONG2NUM((unsigned long)histogram[x].count));
    }

    /*
        Christy evidently didn't agree with Bob's memory management.
    */
    RelinquishMagickMemory(histogram);

    if (dc_copy)
    {
        // Do not trace destruction
        DestroyImage(dc_copy);
    }

    RB_GC_GUARD(hash);
    RB_GC_GUARD(pixel);

    return hash;
}


/**
 * Store all the profiles in the profile in the target image. Called from
 * Image_color_profile_eq and Image_iptc_profile_eq.
 *
 * No Ruby usage (internal function)
 *
 * @param self this object
 * @param name profile name
 * @param profile an IPTC or ICC profile
 * @return self
 */
static VALUE
set_profile(VALUE self, const char *name, VALUE profile)
{
    Image *image, *profile_image;
    ImageInfo *info;
    const MagickInfo *m;
    ExceptionInfo *exception;
    char *profile_name;
    char *profile_blob;
    size_t profile_length;
    const StringInfo *profile_data;

    image = rm_check_frozen(self);

    profile_blob = rm_str2cstr(profile, &profile_length);

    exception = AcquireExceptionInfo();
    m = GetMagickInfo(name, exception);
    CHECK_EXCEPTION();
    if (!m)
    {
        DestroyExceptionInfo(exception);
        rb_raise(rb_eArgError, "unknown name: %s", name);
    }

    info = CloneImageInfo(NULL);
    if (!info)
    {
        DestroyExceptionInfo(exception);
        rb_raise(rb_eNoMemError, "not enough memory to continue");
    }

    strlcpy(info->magick, m->name, sizeof(info->magick));

    GVL_STRUCT_TYPE(BlobToImage) args = { info, profile_blob, profile_length, exception };
    profile_image = (Image *)CALL_FUNC_WITHOUT_GVL(GVL_FUNC(BlobToImage), &args);
    DestroyImageInfo(info);
    CHECK_EXCEPTION();

    ResetImageProfileIterator(profile_image);
    profile_name = GetNextImageProfile(profile_image);
    while (profile_name)
    {
        /* Hack for versions of ImageMagick where the meta coder would change the iptc profile into an 8bim profile */
        if (rm_strcasecmp("8bim", profile_name) == 0 && rm_strcasecmp("iptc", name) == 0)
        {
#if defined(IMAGEMAGICK_7)
            GVL_STRUCT_TYPE(ProfileImage) args = { image, name, profile_blob, profile_length, exception };
            CALL_FUNC_WITHOUT_GVL(GVL_FUNC(ProfileImage), &args);
            if (rm_should_raise_exception(exception, RetainExceptionRetention))
#else
            GVL_STRUCT_TYPE(ProfileImage) args = { image, name, profile_blob, profile_length, MagickFalse };
            CALL_FUNC_WITHOUT_GVL(GVL_FUNC(ProfileImage), &args);
            if (rm_should_raise_exception(&image->exception, RetainExceptionRetention))
#endif
            {
                break;
            }
        }
        else if (rm_strcasecmp(profile_name, name) == 0)
        {
            profile_data = GetImageProfile(profile_image, profile_name);
            if (profile_data)
            {
#if defined(IMAGEMAGICK_7)
                GVL_STRUCT_TYPE(ProfileImage) args = { image, name, GetStringInfoDatum(profile_data), GetStringInfoLength(profile_data), exception };
                CALL_FUNC_WITHOUT_GVL(GVL_FUNC(ProfileImage), &args);
                if (rm_should_raise_exception(exception, RetainExceptionRetention))
#else
                GVL_STRUCT_TYPE(ProfileImage) args = { image, name, GetStringInfoDatum(profile_data), GetStringInfoLength(profile_data), MagickFalse };
                CALL_FUNC_WITHOUT_GVL(GVL_FUNC(ProfileImage), &args);
                if (rm_should_raise_exception(&image->exception, RetainExceptionRetention))
#endif
                {
                    break;
                }
            }
        }
        profile_name = GetNextImageProfile(profile_image);
    }

    DestroyImage(profile_image);

#if defined(IMAGEMAGICK_7)
    CHECK_EXCEPTION();
    DestroyExceptionInfo(exception);
#else
    DestroyExceptionInfo(exception);
    rm_check_image_exception(image, RetainOnError);
#endif

    return self;
}


/**
 * Return the ICC color profile as a String.
 *
 * - If there is no profile, returns ""
 * - This method has no real use but is retained for compatibility with earlier releases of RMagick,
 *   where it had no real use either.
 *
 * @return [String, nil] the ICC color profile
 */
VALUE
Image_color_profile(VALUE self)
{
    Image *image;
    const StringInfo *profile;

    image = rm_check_destroyed(self);
    profile = GetImageProfile(image, "icc");
    if (!profile)
    {
        return Qnil;
    }

    return rb_str_new((char *)profile->datum, (long)profile->length);

}


/**
 * Set the ICC color profile.
 *
 * - Pass nil to remove any existing profile.
 * - Removes any existing profile before adding the new one.
 *
 * @param profile [String, nil] the profile to set
 * @return [String, nil] the given profile
 */
VALUE
Image_color_profile_eq(VALUE self, VALUE profile)
{
    Image_delete_profile(self, rb_str_new2("ICC"));
    if (profile != Qnil)
    {
        set_profile(self, "ICC", profile);
    }
    return profile;
}


/**
 * Change the color value of any pixel that matches target_color and is an immediate neighbor.
 *
 * @param target_color [Magick::Pixel, String] the target color
 * @param fill_color [Magick::Pixel, String] the color to fill
 * @param xv [Numeric] the x position
 * @param yv [Numeric] the y position
 * @param method [Magick::PaintMethod] the method to call
 * @return [Magick::Image] a new image
 * @see Image#opaque
 */
VALUE
Image_color_flood_fill(VALUE self, VALUE target_color, VALUE fill_color,
                       VALUE xv, VALUE yv, VALUE method)
{
    Image *image, *new_image;
    PixelColor target;
    DrawInfo *draw_info;
    PixelColor fill;
    long x, y;
    int fill_method;
    MagickPixel target_mpp;
    MagickBooleanType invert;
#if defined(IMAGEMAGICK_7)
    ExceptionInfo *exception;
#endif

    image = rm_check_destroyed(self);

    // The target and fill args can be either a color name or
    // a Magick::Pixel.
    Color_to_PixelColor(&target, target_color);
    Color_to_PixelColor(&fill, fill_color);

    x = NUM2LONG(xv);
    y = NUM2LONG(yv);
    if ((unsigned long)x > image->columns || (unsigned long)y > image->rows)
    {
        rb_raise(rb_eArgError, "target out of range. %lux%lu given, image is %" RMIuSIZE "x%" RMIuSIZE "",
                 x, y, image->columns, image->rows);
    }

    VALUE_TO_ENUM(method, fill_method, PaintMethod);
    if (!(fill_method == FloodfillMethod || fill_method == FillToBorderMethod))
    {
        rb_raise(rb_eArgError, "paint method must be FloodfillMethod or "
                 "FillToBorderMethod (%d given)", fill_method);
    }

    draw_info = CloneDrawInfo(NULL, NULL);
    if (!draw_info)
    {
        rb_raise(rb_eNoMemError, "not enough memory to continue");
    }
    draw_info->fill = fill;

    new_image = rm_clone_image(image);

    rm_init_magickpixel(new_image, &target_mpp);
    if (fill_method == FillToBorderMethod)
    {
        invert = MagickTrue;
        target_mpp.red   = (MagickRealType) image->border_color.red;
        target_mpp.green = (MagickRealType) image->border_color.green;
        target_mpp.blue  = (MagickRealType) image->border_color.blue;
    }
    else
    {
        invert = MagickFalse;
        target_mpp.red   = (MagickRealType) target.red;
        target_mpp.green = (MagickRealType) target.green;
        target_mpp.blue  = (MagickRealType) target.blue;
    }

#if defined(IMAGEMAGICK_7)
    exception = AcquireExceptionInfo();
    GVL_STRUCT_TYPE(FloodfillPaintImage) args = { new_image, draw_info, &target_mpp, x, y, invert, exception };
    CALL_FUNC_WITHOUT_GVL(GVL_FUNC(FloodfillPaintImage), &args);
    DestroyDrawInfo(draw_info);
    rm_check_exception(exception, new_image, DestroyOnError);
    DestroyExceptionInfo(exception);
#else
    GVL_STRUCT_TYPE(FloodfillPaintImage) args = { new_image, DefaultChannels, draw_info, &target_mpp, x, y, invert };
    CALL_FUNC_WITHOUT_GVL(GVL_FUNC(FloodfillPaintImage), &args);

    DestroyDrawInfo(draw_info);
    rm_check_image_exception(new_image, DestroyOnError);
#endif

    return rm_image_new(new_image);
}


/**
 * Blend the fill color specified by "target" with each pixel in the image. Specify the percentage
 * blend for each r, g, b component.
 *
 * @overload colorize(red, green, blue, target)
 *   @param red [Numeric] The percentage of the fill color red
 *   @param green [Numeric] The percentage of the fill color green
 *   @param blue [Numeric] The percentage of the fill color blue
 *   @param target [Magick::Pixel, String] the color name
 *
 * @overload colorize(red, green, blue, matte, target)
 *   @param red [Numeric] The percentage of the fill color red
 *   @param green [Numeric] The percentage of the fill color green
 *   @param blue [Numeric] The percentage of the fill color blue
 *   @param matte [Numeric] The percentage of the fill color transparency
 *   @param target [Magick::Pixel, String] the color name
 *
 * @return [Magick::Image] a new image
 */
VALUE
Image_colorize(int argc, VALUE *argv, VALUE self)
{
    Image *image, *new_image;
    double red, green, blue, matte;
    char opacity[50];
    PixelColor target;
    ExceptionInfo *exception;

    image = rm_check_destroyed(self);

    if (argc == 4)
    {
        red   = floor(100*NUM2DBL(argv[0])+0.5);
        green = floor(100*NUM2DBL(argv[1])+0.5);
        blue  = floor(100*NUM2DBL(argv[2])+0.5);
        Color_to_PixelColor(&target, argv[3]);
        snprintf(opacity, sizeof(opacity), "%f/%f/%f", red, green, blue);
    }
    else if (argc == 5)
    {
        red   = floor(100*NUM2DBL(argv[0])+0.5);
        green = floor(100*NUM2DBL(argv[1])+0.5);
        blue  = floor(100*NUM2DBL(argv[2])+0.5);
        matte = floor(100*NUM2DBL(argv[3])+0.5);
        Color_to_PixelColor(&target, argv[4]);
        snprintf(opacity, sizeof(opacity), "%f/%f/%f/%f", red, green, blue, matte);
    }
    else
    {
        rb_raise(rb_eArgError, "wrong number of arguments (%d for 4 or 5)", argc);
    }

    exception = AcquireExceptionInfo();
#if defined(IMAGEMAGICK_7)
    GVL_STRUCT_TYPE(ColorizeImage) args = { image, opacity, &target, exception };
#else
    GVL_STRUCT_TYPE(ColorizeImage) args = { image, opacity, target, exception };
#endif
    new_image = (Image *)CALL_FUNC_WITHOUT_GVL(GVL_FUNC(ColorizeImage), &args);
    rm_check_exception(exception, new_image, DestroyOnError);
    DestroyExceptionInfo(exception);

    return rm_image_new(new_image);
}


/**
 * Return the color in the colormap at the specified index. If a new color is specified, replaces
 * the color at the index with the new color.
 *
 * @overload colormap(index)
 *   @param index [Numeric] A number between 0 and the number of colors in the color map. If the
 *     value is out of range, colormap raises an IndexError.  You can get the number of colors in
 *     the color map from the colors attribute.
 *
 * @overload colormap(index, new_color)
 *   @param index [Numeric] A number between 0 and the number of colors in the color map. If the
 *     value is out of range, colormap raises an IndexError.  You can get the number of colors in
 *     the color map from the colors attribute.
 *   @param new_color [Magick::Pixel, String] the color name
 *
 * @return [String] the name of the color at the specified location in the color map
 */
VALUE
Image_colormap(int argc, VALUE *argv, VALUE self)
{
    Image *image;
    unsigned long idx;
    PixelColor color, new_color;

    image = rm_check_destroyed(self);

    // We can handle either 1 or 2 arguments. Nothing else.
    if (argc == 0 || argc > 2)
    {
        rb_raise(rb_eArgError, "wrong number of arguments (%d for 1 or 2)", argc);
    }

    idx = NUM2ULONG(argv[0]);
    if (idx > QuantumRange)
    {
        rb_raise(rb_eIndexError, "index out of range");
    }

    // If this is a simple "get" operation, ensure the image has a colormap.
    if (argc == 1)
    {
        if (!image->colormap)
        {
            rb_raise(rb_eIndexError, "image does not contain a colormap");
        }
        // Validate the index

        if (idx > image->colors-1)
        {
            rb_raise(rb_eIndexError, "index out of range");
        }
        return rm_pixelcolor_to_color_name(image, &image->colormap[idx]);
    }

    // This is a "set" operation. Things are different.

    rb_check_frozen(self);

    // Replace with new color? The arg can be either a color name or
    // a Magick::Pixel.
    Color_to_PixelColor(&new_color, argv[1]);

    // Handle no colormap or current colormap too small.
    if (!image->colormap || idx > image->colors-1)
    {
        PixelColor black;
        unsigned long i;

        memset(&black, 0, sizeof(black));

        if (!image->colormap)
        {
            image->colormap = (PixelColor *)magick_safe_malloc((idx+1), sizeof(PixelColor));
            image->colors = 0;
        }
        else
        {
            image->colormap = (PixelColor *)magick_safe_realloc(image->colormap, (idx+1), sizeof(PixelColor));
        }

        for (i = image->colors; i < idx; i++)
        {
            image->colormap[i] = black;
        }
        image->colors = idx+1;
    }

    // Save the current color so we can return it. Set the new color.
    color = image->colormap[idx];
    image->colormap[idx] = new_color;

    return rm_pixelcolor_to_color_name(image, &color);
}

/**
 * Get the number of colors in the colormap.
 *
 * @return [Integer] the number of colors
 */
VALUE
Image_colors(VALUE self)
{
    IMPLEMENT_TYPED_ATTR_READER(Image, colors, ulong, &rm_image_data_type);
}

/**
 * Return the Image pixel interpretation. If the colorspace is RGB the pixels are red, green,
 * blue. If matte is true, then red, green, blue, and index. If it is CMYK, the pixels are cyan,
 * yellow, magenta, black. Otherwise the colorspace is ignored.
 *
 * @return [Magick::ColorspaceType] the colorspace
 */
VALUE
Image_colorspace(VALUE self)
{
    Image *image;

    image = rm_check_destroyed(self);
    return ColorspaceType_find(image->colorspace);
}


/**
 * Set the image's colorspace.
 *
 * @param colorspace [Magick::ColorspaceType] the colorspace
 * @return [Magick::ColorspaceType] the given colorspace
 */
VALUE
Image_colorspace_eq(VALUE self, VALUE colorspace)
{
    Image *image;
    ColorspaceType new_cs;
#if defined(IMAGEMAGICK_7)
    ExceptionInfo *exception;
#endif

    image = rm_check_frozen(self);
    VALUE_TO_ENUM(colorspace, new_cs, ColorspaceType);

#if defined(IMAGEMAGICK_7)
    exception = AcquireExceptionInfo();
    GVL_STRUCT_TYPE(TransformImageColorspace) args = { image, new_cs, exception };
    CALL_FUNC_WITHOUT_GVL(GVL_FUNC(TransformImageColorspace), &args);
    CHECK_EXCEPTION();
    DestroyExceptionInfo(exception);
#else
    GVL_STRUCT_TYPE(TransformImageColorspace) args = { image, new_cs };
    CALL_FUNC_WITHOUT_GVL(GVL_FUNC(TransformImageColorspace), &args);
    rm_check_image_exception(image, RetainOnError);
#endif

    return colorspace;
}


/**
 * Get image columns.
 *
 * @return [Integer] the columns
 */
VALUE
Image_columns(VALUE self)
{
    IMPLEMENT_TYPED_ATTR_READER(Image, columns, int, &rm_image_data_type);
}


/**
 * Compare one or more channels in two images and returns the specified distortion metric and a
 * comparison image.
 *
 * @overload compare_channel(image, metric, channel = Magick::AllChannels)
 *   @param image [Magick::Image, Magick::ImageList] Either an imagelist or an image. If an
 *     imagelist, uses the current image.
 *   @param metric [Magick::MetricType] The desired distortion metric.
 *   @param channel [Magick::ChannelType] a ChannelType arguments.
 *
 * @overload compare_channel(image, metric, channel = Magick::AllChannels)
 *   When a block is given, compare_channel yields with a block argument you can optionally use to
 *   set attributes.
 *   - options.highlight_color = color
 *     - Emphasize pixel differences with this color. The default is partially transparent red.
 *   - options.lowlight_color = color
 *     - Demphasize pixel differences with this color. The default is partially transparent white.
 *   @param image [Magick::Image, Magick::ImageList] Either an imagelist or an image. If an
 *     imagelist, uses the current image.
 *   @param metric [Magick::MetricType] The desired distortion metric.
 *   @param channel [Magick::ChannelType] a ChannelType arguments.
 *   @yield [opt_args]
 *   @yieldparam opt_args [Magick::OptionalMethodArguments]
 *
 * @overload compare_channel(image, metric, *channels)
 *   @param image [Magick::Image, Magick::ImageList] Either an imagelist or an image. If an
 *     imagelist, uses the current image.
 *   @param metric [Magick::MetricType] The desired distortion metric.
 *   @param *channels [Magick::ChannelType] one or more ChannelType arguments.
 *
 * @overload compare_channel(image, metric, *channels)
 *   When a block is given, compare_channel yields with a block argument you can optionally use to
 *   set attributes.
 *   - options.highlight_color = color
 *     - Emphasize pixel differences with this color. The default is partially transparent red.
 *   - options.lowlight_color = color
 *     - Demphasize pixel differences with this color. The default is partially transparent white.
 *   @param image [Magick::Image, Magick::ImageList] Either an imagelist or an image. If an
 *     imagelist, uses the current image.
 *   @param metric [Magick::MetricType] The desired distortion metric.
 *   @param *channels [Magick::ChannelType] one or more ChannelType arguments.
 *   @yield [opt_args]
 *   @yieldparam opt_args [Magick::OptionalMethodArguments]
 *
 * @return [Array] The first element is a difference image, the second is a the value of the
 *   computed distortion represented as a Float.
 */
VALUE
Image_compare_channel(int argc, VALUE *argv, VALUE self)
{
    Image *image, *r_image, *difference_image;
    double distortion;
    VALUE ary, ref;
    MetricType metric_type;
    ChannelType channels;
    ExceptionInfo *exception;

    image = rm_check_destroyed(self);

    channels = extract_channels(&argc, argv);

    if (argc > 2)
    {
        raise_ChannelType_error(argv[argc-1]);
    }
    if (argc != 2)
    {
        rb_raise(rb_eArgError, "wrong number of arguments (%d for 2 or more)", argc);
    }

    rm_get_optional_arguments(self);

    ref = rm_cur_image(argv[0]);
    r_image = rm_check_destroyed(ref);

    VALUE_TO_ENUM(argv[1], metric_type, MetricType);

    exception = AcquireExceptionInfo();
#if defined(IMAGEMAGICK_7)
    BEGIN_CHANNEL_MASK(image, channels);
    GVL_STRUCT_TYPE(CompareImages) args = { image, r_image, metric_type, &distortion, exception };
    difference_image = (Image *)CALL_FUNC_WITHOUT_GVL(GVL_FUNC(CompareImages), &args);
    END_CHANNEL_MASK(image);
#else
    GVL_STRUCT_TYPE(CompareImageChannels) args = { image, r_image, channels, metric_type, &distortion, exception };
    difference_image = (Image *)CALL_FUNC_WITHOUT_GVL(GVL_FUNC(CompareImageChannels), &args);
#endif
    rm_check_exception(exception, difference_image, DestroyOnError);
    DestroyExceptionInfo(exception);

    ary = rb_ary_new2(2);
    rb_ary_store(ary, 0, rm_image_new(difference_image));
    rb_ary_store(ary, 1, rb_float_new(distortion));

    RB_GC_GUARD(ary);
    RB_GC_GUARD(ref);

    return ary;
}


/**
 * Return the composite operator attribute.
 *
 * @return [Magick::CompositeOperator] the composite operator
 */
VALUE
Image_compose(VALUE self)
{
    Image *image = rm_check_destroyed(self);
    return CompositeOperator_find(image->compose);
}


/**
 * Set the composite operator attribute.
 *
 * @param compose_arg [Magick::CompositeOperator] the composite operator
 * @return [Magick::CompositeOperator] the given value
 */
VALUE
Image_compose_eq(VALUE self, VALUE compose_arg)
{
    Image *image = rm_check_frozen(self);
    VALUE_TO_ENUM(compose_arg, image->compose, CompositeOperator);
    return compose_arg;
}

/**
 * Call CompositeImage.
 *
 * No Ruby usage (internal function)
 *
 * Notes:
 *   - The other image can be either an Image or an Image.
 *   - The use of the GravityType to position the composited image is based on
 *     Magick++.
 *   - The `gravity' argument has the same effect as the -gravity option does in
 *     the `composite' utility.
 *
 * @param bang whether the bang (!) version of the method was called
 * @param argc number of input arguments
 * @param argv array of input arguments
 * @param self this object
 * @param channels
 * @return self if bang, otherwise new composited image
 * @see Image_composite
 * @see Image_composite_bang
 */
static VALUE
composite(int bang, int argc, VALUE *argv, VALUE self, ChannelType channels)
{
    Image *image, *new_image;
    Image *comp_image;
    CompositeOperator composite_op = UndefinedCompositeOp;
    GravityType gravity;
    VALUE comp;
    signed long x_offset = 0;
    signed long y_offset = 0;
#if defined(IMAGEMAGICK_7)
    ExceptionInfo *exception;
#endif

    image = rm_check_destroyed(self);

    if (bang)
    {
        rb_check_frozen(self);
    }
    if (argc < 3 || argc > 5)
    {
        rb_raise(rb_eArgError, "wrong number of arguments (%d for 3, 4, or 5)", argc);
    }


    comp = rm_cur_image(argv[0]);
    comp_image = rm_check_destroyed(comp);
    RB_GC_GUARD(comp);

    switch (argc)
    {
        case 3:                 // argv[1] is gravity, argv[2] is composite_op
            VALUE_TO_ENUM(argv[1], gravity, GravityType);
            VALUE_TO_ENUM(argv[2], composite_op, CompositeOperator);

            // convert gravity to x, y offsets
            switch (gravity)
            {
                case ForgetGravity:
                case NorthWestGravity:
                    x_offset = 0;
                    y_offset = 0;
                    break;
                case NorthGravity:
                    x_offset = ((long)(image->columns) - (long)(comp_image->columns)) / 2;
                    y_offset = 0;
                    break;
                case NorthEastGravity:
                    x_offset = (long)(image->columns) - (long)(comp_image->columns);
                    y_offset = 0;
                    break;
                case WestGravity:
                    x_offset = 0;
                    y_offset = ((long)(image->rows) - (long)(comp_image->rows)) / 2;
                    break;
                case CenterGravity:
                default:
                    x_offset = ((long)(image->columns) - (long)(comp_image->columns)) / 2;
                    y_offset = ((long)(image->rows) - (long)(comp_image->rows)) / 2;
                    break;
                case EastGravity:
                    x_offset = (long)(image->columns) - (long)(comp_image->columns);
                    y_offset = ((long)(image->rows) - (long)(comp_image->rows)) / 2;
                    break;
                case SouthWestGravity:
                    x_offset = 0;
                    y_offset = (long)(image->rows) - (long)(comp_image->rows);
                    break;
                case SouthGravity:
                    x_offset = ((long)(image->columns) - (long)(comp_image->columns)) / 2;
                    y_offset = (long)(image->rows) - (long)(comp_image->rows);
                    break;
                case SouthEastGravity:
                    x_offset = (long)(image->columns) - (long)(comp_image->columns);
                    y_offset = (long)(image->rows) - (long)(comp_image->rows);
                    break;
            }
            break;

        case 4:                 // argv[1], argv[2] is x_off, y_off,
            // argv[3] is composite_op
            x_offset = NUM2LONG(argv[1]);
            y_offset = NUM2LONG(argv[2]);
            VALUE_TO_ENUM(argv[3], composite_op, CompositeOperator);
            break;

        case 5:
            VALUE_TO_ENUM(argv[1], gravity, GravityType);
            x_offset = NUM2LONG(argv[2]);
            y_offset = NUM2LONG(argv[3]);
            VALUE_TO_ENUM(argv[4], composite_op, CompositeOperator);

            switch (gravity)
            {
                case NorthEastGravity:
                case EastGravity:
                case SouthEastGravity:
                    x_offset = ((long)(image->columns) - (long)(comp_image->columns)) - x_offset;
                    break;
                case NorthGravity:
                case SouthGravity:
                case CenterGravity:
                    x_offset += (long)(image->columns/2) - (long)(comp_image->columns/2);
                    break;
                default:
                    break;
            }
            switch (gravity)
            {
                case SouthWestGravity:
                case SouthGravity:
                case SouthEastGravity:
                    y_offset = ((long)(image->rows) - (long)(comp_image->rows)) - y_offset;
                    break;
                case EastGravity:
                case WestGravity:
                case CenterGravity:
                    y_offset += (long)(image->rows/2) - (long)(comp_image->rows/2);
                    break;
                case NorthEastGravity:
                case NorthGravity:
                default:
                    break;
            }
            break;

    }

    if (bang)
    {
#if defined(IMAGEMAGICK_7)
        exception = AcquireExceptionInfo();
        BEGIN_CHANNEL_MASK(image, channels);
        GVL_STRUCT_TYPE(CompositeImage) args = { image, comp_image, composite_op, MagickTrue, x_offset, y_offset, exception };
        CALL_FUNC_WITHOUT_GVL(GVL_FUNC(CompositeImage), &args);
        END_CHANNEL_MASK(image);
        CHECK_EXCEPTION();
        DestroyExceptionInfo(exception);
#else
        GVL_STRUCT_TYPE(CompositeImageChannel) args = { image, channels, composite_op, comp_image, x_offset, y_offset };
        CALL_FUNC_WITHOUT_GVL(GVL_FUNC(CompositeImageChannel), &args);
        rm_check_image_exception(image, RetainOnError);
#endif

        return self;
    }
    else
    {
        new_image = rm_clone_image(image);

#if defined(IMAGEMAGICK_7)
        exception = AcquireExceptionInfo();
        BEGIN_CHANNEL_MASK(new_image, channels);
        GVL_STRUCT_TYPE(CompositeImage) args = { new_image, comp_image, composite_op, MagickTrue, x_offset, y_offset, exception };
        CALL_FUNC_WITHOUT_GVL(GVL_FUNC(CompositeImage), &args);
        END_CHANNEL_MASK(new_image);
        rm_check_exception(exception, new_image, DestroyOnError);
        DestroyExceptionInfo(exception);
#else
        GVL_STRUCT_TYPE(CompositeImageChannel) args = { new_image, channels, composite_op, comp_image, x_offset, y_offset };
        CALL_FUNC_WITHOUT_GVL(GVL_FUNC(CompositeImageChannel), &args);
        rm_check_image_exception(new_image, DestroyOnError);
#endif

        return rm_image_new(new_image);
    }
}


/**
 * Composites src onto dest using the specified composite operator.
 * In-place form of {Magick::Image#composite}.
 *
 * @overload composite!(image, x_off, y_off, composite_op)
 *   @param image [Magick::Image, Magick::ImageList] Either an imagelist or an image. If an
 *     imagelist, uses the current image.
 *   @param x_off [Numeric] the x-offset of the composited image, measured from the upper-left
 *     corner of the image.
 *   @param y_off [Numeric] the y-offset of the composited image, measured from the upper-left
 *     corner of the image.
 *   @param composite_op [Magick::CompositeOperator] the composite operator
 *
 * @overload composite!(image, gravity, composite_op)
 *   @param image [Magick::Image, Magick::ImageList] Either an imagelist or an image. If an
 *     imagelist, uses the current image.
 *   @param gravity [Magick::GravityType] A GravityType value that specifies the location of img on
 *     image.
 *   @param composite_op [Magick::CompositeOperator] the composite operator
 *
 * @overload composite!(image, gravity, x_off, y_off, composite_op)
 *   @param image [Magick::Image, Magick::ImageList] Either an imagelist or an image. If an
 *     imagelist, uses the current image.
 *   @param gravity [Magick::GravityType] A GravityType value that specifies the location of img on
 *     image.
 *   @param x_off [Numeric] the x-offset of the composited image, measured from the upper-left
 *     corner of the image.
 *   @param y_off [Numeric] the y-offset of the composited image, measured from the upper-left
 *     corner of the image.
 *   @param composite_op [Magick::CompositeOperator] the composite operator.
 *
 * @return [Magick::Image] a new image
 * @see Image#composite
 */
VALUE
Image_composite_bang(int argc, VALUE *argv, VALUE self)
{
    return composite(True, argc, argv, self, DefaultChannels);
}


/**
 * Composites src onto dest using the specified composite operator.
 *
 * @overload composite(image, x_off, y_off, composite_op)
 *   @param image [Magick::Image, Magick::ImageList] Either an imagelist or an image. If an
 *     imagelist, uses the current image.
 *   @param x_off [Numeric] the x-offset of the composited image, measured from the upper-left
 *     corner of the image.
 *   @param y_off [Numeric] the y-offset of the composited image, measured from the upper-left
 *     corner of the image.
 *   @param composite_op [Magick::CompositeOperator] the composite operator
 *
 * @overload composite(image, gravity, composite_op)
 *   @param image [Magick::Image, Magick::ImageList] Either an imagelist or an image. If an
 *     imagelist, uses the current image.
 *   @param gravity [Magick::GravityType] A GravityType value that specifies the location of img on
 *     image.
 *   @param composite_op [Magick::CompositeOperator] the composite operator
 *
 * @overload composite(image, gravity, x_off, y_off, composite_op)
 *   @param image [Magick::Image, Magick::ImageList] Either an imagelist or an image. If an
 *     imagelist, uses the current image.
 *   @param gravity [Magick::GravityType] A GravityType value that specifies the location of img on
 *     image.
 *   @param x_off [Numeric] the x-offset of the composited image, measured from the upper-left
 *     corner of the image.
 *   @param y_off [Numeric] the y-offset of the composited image, measured from the upper-left
 *     corner of the image.
 *   @param composite_op [Magick::CompositeOperator] the composite operator.
 *
 * @return [Magick::Image] a new image
 * @see Image#composite!
 */
VALUE
Image_composite(int argc, VALUE *argv, VALUE self)
{
    return composite(False, argc, argv, self, DefaultChannels);
}


/**
 * Composite the source over the destination image as dictated by the affine transform.
 *
 * @param source [Magick::Image, Magick::ImageList] the source image
 * @param affine_matrix [Magick::AffineMatrix] affine transform matrix
 * @return [Magick::Image] a new image
 */
VALUE
Image_composite_affine(VALUE self, VALUE source, VALUE affine_matrix)
{
    Image *image, *composite_image, *new_image;
    AffineMatrix affine;
#if defined(IMAGEMAGICK_7)
    ExceptionInfo *exception;
#endif

    image = rm_check_destroyed(self);
    composite_image = rm_check_destroyed(rm_cur_image(source));

    Export_AffineMatrix(&affine, affine_matrix);
    new_image = rm_clone_image(image);

#if defined(IMAGEMAGICK_7)
    exception = AcquireExceptionInfo();
    GVL_STRUCT_TYPE(DrawAffineImage) args = { new_image, composite_image, &affine, exception };
    CALL_FUNC_WITHOUT_GVL(GVL_FUNC(DrawAffineImage), &args);
    rm_check_exception(exception, new_image, DestroyOnError);
    DestroyExceptionInfo(exception);
#else
    GVL_STRUCT_TYPE(DrawAffineImage) args = { new_image, composite_image, &affine };
    CALL_FUNC_WITHOUT_GVL(GVL_FUNC(DrawAffineImage), &args);
    rm_check_image_exception(new_image, DestroyOnError);
#endif

    return rm_image_new(new_image);
}


/**
 * Call CompositeImageChannel.
 *
 * No Ruby usage (internal function)
 *
 * Notes:
 *   - Default channel is AllChannels
 *
 * @param bang whether the bang (!) version of the method was called
 * @param argc number of input arguments
 * @param argv array of input arguments
 * @param self this object
 * @return self if bang, otherwise a new image
 * @see Image_composite_channel
 * @see Image_composite_channel_bang
 */
static VALUE
composite_channel(int bang, int argc, VALUE *argv, VALUE self)
{
    ChannelType channels;

    // Check destroyed before validating the arguments
    rm_check_destroyed(self);
    channels = extract_channels(&argc, argv);

    // There must be 3, 4, or 5 remaining arguments.
    if (argc < 3)
    {
        rb_raise(rb_eArgError, "composite operator not specified");
    }
    else if (argc > 5)
    {
        raise_ChannelType_error(argv[argc-1]);
    }

    return composite(bang, argc, argv, self, channels);
}


/**
 * Composite the source over the destination image channel as dictated by the affine transform.
 *
 * @overload composite_channel(image, x_off, y_off, composite_op, channel = Magick::AllChannels)
 *   @param image [Magick::Image, Magick::ImageList] Either an imagelist or an image. If an
 *     imagelist, uses the current image.
 *   @param x_off [Numeric] the x-offset of the composited image, measured from the upper-left
 *     corner of the image.
 *   @param y_off [Numeric] the y-offset of the composited image, measured from the upper-left
 *     corner of the image.
 *   @param composite_op [Magick::CompositeOperator] the composite operator
 *   @param channel [Magick::ChannelType] a ChannelType arguments.
 *
 * @overload composite_channel(image, x_off, y_off, composite_op, *channels)
 *   @param image [Magick::Image, Magick::ImageList] Either an imagelist or an image. If an
 *     imagelist, uses the current image.
 *   @param x_off [Numeric] the x-offset of the composited image, measured from the upper-left
 *     corner of the image.
 *   @param y_off [Numeric] the y-offset of the composited image, measured from the upper-left
 *     corner of the image.
 *   @param composite_op [Magick::CompositeOperator] the composite operator
 *   @param *channels [Magick::ChannelType] one or more ChannelType arguments.
 *
 * @overload composite_channel(image, gravity, composite_op, channel = Magick::AllChannels)
 *   @param image [Magick::Image, Magick::ImageList] Either an imagelist or an image. If an
 *     imagelist, uses the current image.
 *   @param gravity [Magick::GravityType] A GravityType value that specifies the location of img on
 *     image.
 *   @param composite_op [Magick::CompositeOperator] the composite operator
 *   @param channel [Magick::ChannelType] a ChannelType arguments.
 *
 * @overload composite_channel(image, gravity, composite_op, *channels)
 *   @param image [Magick::Image, Magick::ImageList] Either an imagelist or an image. If an
 *     imagelist, uses the current image.
 *   @param gravity [Magick::GravityType] A GravityType value that specifies the location of img on
 *     image.
 *   @param composite_op [Magick::CompositeOperator] the composite operator
 *   @param *channels [Magick::ChannelType] one or more ChannelType arguments.
 *
 * @overload composite_channel(image, gravity, x_off, y_off, composite_op, channel = Magick::AllChannels)
 *   @param image [Magick::Image, Magick::ImageList] Either an imagelist or an image. If an
 *     imagelist, uses the current image.
 *   @param gravity [Magick::GravityType] A GravityType value that specifies the location of img on
 *     image.
 *   @param x_off [Numeric] the x-offset of the composited image, measured from the upper-left
 *     corner of the image.
 *   @param y_off [Numeric] the y-offset of the composited image, measured from the upper-left
 *     corner of the image.
 *   @param composite_op [Magick::CompositeOperator] the composite operator
 *   @param channel [Magick::ChannelType] a ChannelType arguments.
 *
 * @overload composite_channel(image, gravity, x_off, y_off, composite_op, *channels)
 *   @param image [Magick::Image, Magick::ImageList] Either an imagelist or an image. If an
 *     imagelist, uses the current image.
 *   @param gravity [Magick::GravityType] A GravityType value that specifies the location of img on
 *     image.
 *   @param x_off [Numeric] the x-offset of the composited image, measured from the upper-left
 *     corner of the image.
 *   @param y_off [Numeric] the y-offset of the composited image, measured from the upper-left
 *     corner of the image.
 *   @param composite_op [Magick::CompositeOperator] the composite operator
 *   @param *channels [Magick::ChannelType] one or more ChannelType arguments.
 *
 * @return [Magick::Image] a new image
 * @see Image#composite
 */
VALUE
Image_composite_channel(int argc, VALUE *argv, VALUE self)
{
    return composite_channel(False, argc, argv, self);
}


/**
 * Composite the source over the destination image channel as dictated by the affine transform.
 * In-place form of {Image#composite_channel}.
 *
 * @overload composite_channel!(image, x_off, y_off, composite_op, channel = Magick::AllChannels)
 *   @param image [Magick::Image, Magick::ImageList] Either an imagelist or an image. If an
 *     imagelist, uses the current image.
 *   @param x_off [Numeric] the x-offset of the composited image, measured from the upper-left
 *     corner of the image.
 *   @param y_off [Numeric] the y-offset of the composited image, measured from the upper-left
 *     corner of the image.
 *   @param composite_op [Magick::CompositeOperator] the composite operator
 *   @param channel [Magick::ChannelType] a ChannelType arguments.
 *
 * @overload composite_channel!(image, x_off, y_off, composite_op, *channels)
 *   @param image [Magick::Image, Magick::ImageList] Either an imagelist or an image. If an
 *     imagelist, uses the current image.
 *   @param x_off [Numeric] the x-offset of the composited image, measured from the upper-left
 *     corner of the image.
 *   @param y_off [Numeric] the y-offset of the composited image, measured from the upper-left
 *     corner of the image.
 *   @param composite_op [Magick::CompositeOperator] the composite operator
 *   @param *channels [Magick::ChannelType] one or more ChannelType arguments.
 *
 * @overload composite_channel!(image, gravity, composite_op, channel = Magick::AllChannels)
 *   @param image [Magick::Image, Magick::ImageList] Either an imagelist or an image. If an
 *     imagelist, uses the current image.
 *   @param gravity [Magick::GravityType] A GravityType value that specifies the location of img on
 *     image.
 *   @param composite_op [Magick::CompositeOperator] the composite operator
 *   @param channel [Magick::ChannelType] a ChannelType arguments.
 *
 * @overload composite_channel!(image, gravity, composite_op, *channels)
 *   @param image [Magick::Image, Magick::ImageList] Either an imagelist or an image. If an
 *     imagelist, uses the current image.
 *   @param gravity [Magick::GravityType] A GravityType value that specifies the location of img on
 *     image.
 *   @param composite_op [Magick::CompositeOperator] the composite operator
 *   @param *channels [Magick::ChannelType] one or more ChannelType arguments.
 *
 * @overload composite_channel!(image, gravity, x_off, y_off, composite_op, channel = Magick::AllChannels)
 *   @param image [Magick::Image, Magick::ImageList] Either an imagelist or an image. If an
 *     imagelist, uses the current image.
 *   @param gravity [Magick::GravityType] A GravityType value that specifies the location of img on
 *     image.
 *   @param x_off [Numeric] the x-offset of the composited image, measured from the upper-left
 *     corner of the image.
 *   @param y_off [Numeric] the y-offset of the composited image, measured from the upper-left
 *     corner of the image.
 *   @param composite_op [Magick::CompositeOperator] the composite operator
 *   @param channel [Magick::ChannelType] a ChannelType arguments.
 *
 * @overload composite_channel!(image, gravity, x_off, y_off, composite_op, *channels)
 *   @param image [Magick::Image, Magick::ImageList] Either an imagelist or an image. If an
 *     imagelist, uses the current image.
 *   @param gravity [Magick::GravityType] A GravityType value that specifies the location of img on
 *     image.
 *   @param x_off [Numeric] the x-offset of the composited image, measured from the upper-left
 *     corner of the image.
 *   @param y_off [Numeric] the y-offset of the composited image, measured from the upper-left
 *     corner of the image.
 *   @param composite_op [Magick::CompositeOperator] the composite operator
 *   @param *channels [Magick::ChannelType] one or more ChannelType arguments.
 *
 * @return [Magick::Image] a new image
 * @see Image#composite_channel
 * @see Image#composite!
 */
VALUE
Image_composite_channel_bang(int argc, VALUE *argv, VALUE self)
{
    return composite_channel(True, argc, argv, self);
}


/**
 * Merge the source and destination images according to the formula
 *   a*Sc*Dc + b*Sc + c*Dc + d
 * where Sc is the source pixel and Dc is the destination pixel.
 *
 * @overload composite_mathematics(image, a, b, c, d, gravity)
 *   @param image [Magick::Image, Magick::ImageList] Either an imagelist or an image. If an
 *     imagelist, uses the current image.
 *   @param a [Numeric] See the description.
 *   @param b [Numeric] See the description.
 *   @param c [Numeric] See the description.
 *   @param d [Numeric] See the description.
 *   @param gravity [Magick::GravityType] the gravity type
 *
 * @overload composite_mathematics(image, a, b, c, d, x_off, y_off)
 *   @param image [Magick::Image, Magick::ImageList] Either an imagelist or an image. If an
 *     imagelist, uses the current image.
 *   @param a [Numeric] See the description.
 *   @param b [Numeric] See the description.
 *   @param c [Numeric] See the description.
 *   @param d [Numeric] See the description.
 *   @param x_off [Numeric] The x-offset of the composited image, measured relative to the gravity
 *     argument.
 *   @param y_off [Numeric] The y-offset of the composited image, measured relative to the gravity
 *     argument.
 *
 * @overload composite_mathematics(image, a, b, c, d, gravity, x_off, y_off)
 *   @param image [Magick::Image, Magick::ImageList] Either an imagelist or an image. If an
 *     imagelist, uses the current image.
 *   @param a [Numeric] See the description.
 *   @param b [Numeric] See the description.
 *   @param c [Numeric] See the description.
 *   @param d [Numeric] See the description.
 *   @param gravity [Magick::GravityType] the gravity type
 *   @param x_off [Numeric] The x-offset of the composited image, measured relative to the gravity
 *     argument.
 *   @param y_off [Numeric] The y-offset of the composited image, measured relative to the gravity
 *     argument.
 *
 * @return [Magick::Image] a new image
 */
VALUE
Image_composite_mathematics(int argc, VALUE *argv, VALUE self)
{
    Image *composite_image;
    VALUE args[5];
    signed long x_off = 0L;
    signed long y_off = 0L;
    GravityType gravity = NorthWestGravity;
    char compose_args[200];

    rm_check_destroyed(self);

    switch (argc)
    {
        case 8:
            VALUE_TO_ENUM(argv[5], gravity, GravityType);
            x_off = NUM2LONG(argv[6]);
            y_off = NUM2LONG(argv[7]);
            break;
        case 7:
            x_off = NUM2LONG(argv[5]);
            y_off = NUM2LONG(argv[6]);
            break;
        case 6:
            VALUE_TO_ENUM(argv[5], gravity, GravityType);
            break;
        default:
            rb_raise(rb_eArgError, "wrong number of arguments (got %d, expected 6 to 8)", argc);
            break;
    }

    composite_image = rm_check_destroyed(rm_cur_image(argv[0]));

    snprintf(compose_args, sizeof(compose_args), "%-.16g,%-.16g,%-.16g,%-.16g", NUM2DBL(argv[1]), NUM2DBL(argv[2]), NUM2DBL(argv[3]), NUM2DBL(argv[4]));
    SetImageArtifact(composite_image, "compose:args", compose_args);

    // Call composite(False, gravity, x_off, y_off, MathematicsCompositeOp, DefaultChannels)
    args[0] = argv[0];
    args[1] = GravityType_find(gravity);
    args[2] = LONG2FIX(x_off);
    args[3] = LONG2FIX(y_off);
    args[4] = CompositeOperator_find(MathematicsCompositeOp);

    return composite(False, 5, args, self, DefaultChannels);
}


/**
 * Emulate the -tile option to the composite command.
 *
 * No Ruby usage (internal function)
 *
 * Notes:
 *   - Default composite_op is Magick::OverCompositeOp
 *   - Default channel is AllChannels
 *
 * @param bang whether the bang (!) version of the method was called
 * @param argc number of input arguments
 * @param argv array of input arguments
 * @param self this object
 * @return self if bang, otherwise a new image
 * @see Image_composite_tiled
 * @see Image_composite_tiled_bang
 * @see wand/composite.c in ImageMagick (6.2.4)
 */
static VALUE
composite_tiled(int bang, int argc, VALUE *argv, VALUE self)
{
    Image *image;
    Image *comp_image;
    CompositeOperator composite_op = OverCompositeOp;
    long x, y;
    unsigned long columns;
    ChannelType channels;
    MagickStatusType status;
#if defined(IMAGEMAGICK_7)
    ExceptionInfo *exception;
#endif

    // Ensure image and composite_image aren't destroyed.
    if (bang)
    {
        image = rm_check_frozen(self);
    }
    else
    {
        image = rm_check_destroyed(self);
    }

    channels = extract_channels(&argc, argv);

    switch (argc)
    {
        case 2:
            VALUE_TO_ENUM(argv[1], composite_op, CompositeOperator);
        case 1:
            break;
        case 0:
            rb_raise(rb_eArgError, "wrong number of arguments (0 for 1 or more)");
            break;
        default:
            raise_ChannelType_error(argv[argc-1]);
            break;
    }

    comp_image = rm_check_destroyed(rm_cur_image(argv[0]));

    if (!bang)
    {
        image = rm_clone_image(image);
    }

    SetImageArtifact(comp_image, "modify-outside-overlay", "false");

    status = MagickTrue;
    columns = comp_image->columns;

#if defined(IMAGEMAGICK_7)
    exception = AcquireExceptionInfo();
#endif

    // Tile
    for (y = 0; y < (long) image->rows; y += comp_image->rows)
    {
        for (x = 0; status == MagickTrue && x < (long) image->columns; x += columns)
        {
#if defined(IMAGEMAGICK_7)
            BEGIN_CHANNEL_MASK(image, channels);
            GVL_STRUCT_TYPE(CompositeImage) args = { image, comp_image, composite_op, MagickTrue, x, y, exception };
            void *ret = CALL_FUNC_WITHOUT_GVL(GVL_FUNC(CompositeImage), &args);
            status = reinterpret_cast<MagickStatusType &>(ret);
            END_CHANNEL_MASK(image);
            rm_check_exception(exception, image, bang ? RetainOnError: DestroyOnError);
#else
            GVL_STRUCT_TYPE(CompositeImageChannel) args = { image, channels, composite_op, comp_image, x, y };
            void *ret = CALL_FUNC_WITHOUT_GVL(GVL_FUNC(CompositeImageChannel), &args);
            status = reinterpret_cast<MagickStatusType &>(ret);
            rm_check_image_exception(image, bang ? RetainOnError: DestroyOnError);
#endif
        }
    }

#if defined(IMAGEMAGICK_7)
    DestroyExceptionInfo(exception);
#endif

    return bang ? self : rm_image_new(image);
}


/**
 * Composites multiple copies of the source image across and down the image,
 * producing the same results as ImageMagick's composite command with the -tile option.
 *
 * @overload composite_tiled(src, composite_op = Magick::OverCompositeOp, channel = Magick::AllChannels)
 *   @param src [Magick::Image, Magick::ImageList] Either an imagelist or an image. If an
 *     imagelist, uses the current image.
 *   @param composite_op [Magick::CompositeOperator] the composite operator
 *   @param channel [Magick::ChannelType] a ChannelType arguments.
 *
 * @overload composite_tiled(src, composite_op = Magick::OverCompositeOp, *channels)
 *   @param src [Magick::Image, Magick::ImageList] Either an imagelist or an image. If an
 *     imagelist, uses the current image.
 *   @param composite_op [Magick::CompositeOperator] the composite operator
 *   @param *channels [Magick::ChannelType] one or more ChannelType arguments.
 *
 * @return [Magick::Image] a new image
 * @see Image#composite_tiled!
 */
VALUE
Image_composite_tiled(int argc, VALUE *argv, VALUE self)
{
    return composite_tiled(False, argc, argv, self);
}


/**
 * Composites multiple copies of the source image across and down the image, producing the same
 * results as ImageMagick's composite command with the -tile option.
 * In-place form of {Magick::Image#composite_tiled}.
 *
 * @overload composite_tiled!(src, composite_op = Magick::OverCompositeOp, channel = Magick::AllChannels)
 *   @param src [Magick::Image, Magick::ImageList] Either an imagelist or an image. If an
 *     imagelist, uses the current image.
 *   @param composite_op [Magick::CompositeOperator] the composite operator
 *   @param channel [Magick::ChannelType] a ChannelType arguments.
 *
 * @overload composite_tiled!(src, composite_op = Magick::OverCompositeOp, *channels)
 *   @param src [Magick::Image, Magick::ImageList] Either an imagelist or an image. If an
 *     imagelist, uses the current image.
 *   @param composite_op [Magick::CompositeOperator] the composite operator
 *   @param *channels [Magick::ChannelType] one or more ChannelType arguments.
 *
 * @return [Magick::Image] a new image
 * @see Image#composite_tiled
 */
VALUE
Image_composite_tiled_bang(int argc, VALUE *argv, VALUE self)
{
    return composite_tiled(True, argc, argv, self);
}


/**
 * Get the compression attribute.
 *
 * @return [Magick::CompressionType] the compression
 */
VALUE
Image_compression(VALUE self)
{
    Image *image = rm_check_destroyed(self);
    return CompressionType_find(image->compression);
}

/**
 * Set the compression attribute.
 *
 * @param compression [Magick::CompressionType] the compression
 * @return [Magick::CompressionType] the given compression
 */
VALUE
Image_compression_eq(VALUE self, VALUE compression)
{
    Image *image = rm_check_frozen(self);
    VALUE_TO_ENUM(compression, image->compression, CompressionType);
    return compression;
}

/**
 * Removes duplicate or unused entries in the colormap.
 * Only PseudoClass images have a colormap.
 * If the image is DirectClass then compress_colormap! converts it to PseudoClass.
 *
 * @return [Magick::Image] self
 */
VALUE
Image_compress_colormap_bang(VALUE self)
{
    Image *image;
    MagickBooleanType okay;
#if defined(IMAGEMAGICK_7)
    ExceptionInfo *exception;
#endif

    image = rm_check_frozen(self);

#if defined(IMAGEMAGICK_7)
    exception = AcquireExceptionInfo();
    GVL_STRUCT_TYPE(CompressImageColormap) args = { image, exception };
    void *ret = CALL_FUNC_WITHOUT_GVL(GVL_FUNC(CompressImageColormap), &args);
    okay = static_cast<MagickBooleanType>(reinterpret_cast<intptr_t &>(ret));
    CHECK_EXCEPTION();
    DestroyExceptionInfo(exception);
#else
    GVL_STRUCT_TYPE(CompressImageColormap) args = { image };
    void *ret = CALL_FUNC_WITHOUT_GVL(GVL_FUNC(CompressImageColormap), &args);
    okay = static_cast<MagickBooleanType>(reinterpret_cast<intptr_t &>(ret));
    rm_check_image_exception(image, RetainOnError);
#endif
    if (!okay)
    {
        rb_warning("CompressImageColormap failed (probably DirectClass image)");
    }

    return self;
}

/**
 * Creates an Image from the supplied pixel data. The pixel data must be in scanline order,
 * top-to-bottom. The pixel data is an array of either all Fixed or all Float elements. If Fixed,
 * the elements must be in the range [0..QuantumRange]. If Float, the elements must be normalized
 * [0..1]. The "map" argument reflects the expected ordering of the pixel array. It can be any
 * combination or order of R = red, G = green, B = blue, A = alpha, C = cyan, Y = yellow, M =
 * magenta, K = black, or I = intensity (for grayscale).
 *
 * The pixel array must have width X height X strlen(map) elements.
 *
 * @param width_arg [Numeric] The number of columns in the image
 * @param height_arg [Numeric] The number of rows in the image
 * @param map_arg [String] A string describing the expected ordering of the pixel array.
 *   It can be any combination or order of R = red, G = green, B = blue, A = alpha, C = cyan, Y =
 *   yellow, M = magenta, K = black, or I = intensity (for grayscale).
 * @param pixels_arg [Array<Magick::Pixel>] The pixel data in the array must be stored in scanline order,
 *   left-to-right and top-to-bottom. The elements in the array must be either all Integers or all
 *   Floats. If the elements are Integers, the Integers must be in the range [0..QuantumRange]. If
 *   the elements are Floats, they must be in the range [0..1].
 * @return [Magick::Image] a new image
 */
VALUE
Image_constitute(VALUE klass ATTRIBUTE_UNUSED, VALUE width_arg, VALUE height_arg,
                 VALUE map_arg, VALUE pixels_arg)
{
    Image *new_image;
    VALUE pixel, pixel0;
    long x, npixels;
    size_t width, height, map_l;
    char *map;
    volatile union
    {
        double *f;
        Quantum *i;
        void *v;
    } pixels;
    VALUE pixel_class;
    StorageType stg_type;
    ExceptionInfo *exception;

    // rb_Array converts objects that are not Arrays to Arrays if possible,
    // and raises TypeError if it can't.
    pixels_arg = rb_Array(pixels_arg);

    if (NUM2LONG(width_arg) <= 0 || NUM2LONG(height_arg) <= 0)
    {
        rb_raise(rb_eArgError, "width and height must be greater than zero");
    }

    width = NUM2LONG(width_arg);
    height = NUM2LONG(height_arg);
    map = rm_str2cstr(map_arg, &map_l);

    npixels = width * height * map_l;
    if (RARRAY_LEN(pixels_arg) != npixels)
    {
        rb_raise(rb_eArgError, "wrong number of array elements (%ld for %ld)",
                 RARRAY_LEN(pixels_arg), npixels);
    }

    // Inspect the first element in the pixels array to determine the expected
    // type of all the elements. Allocate the pixel buffer.
    pixel0 = rb_ary_entry(pixels_arg, 0);
    if (rb_obj_is_kind_of(pixel0, rb_cFloat) == Qtrue)
    {
        pixels.f = ALLOC_N(double, npixels);
        stg_type = DoublePixel;
        pixel_class = rb_cFloat;
    }
    else if (rb_obj_is_kind_of(pixel0, rb_cInteger) == Qtrue)
    {
        pixels.i = ALLOC_N(Quantum, npixels);
        stg_type = QuantumPixel;
        pixel_class = rb_cInteger;
    }
    else
    {
        rb_raise(rb_eTypeError, "element 0 in pixel array is %s, must be numeric",
                 rb_class2name(CLASS_OF(pixel0)));
    }



    // Convert the array elements to the appropriate C type, store in pixel
    // buffer.
    for (x = 0; x < npixels; x++)
    {
        pixel = rb_ary_entry(pixels_arg, x);
        if (rb_obj_is_kind_of(pixel, pixel_class) != Qtrue)
        {
            xfree(pixels.v);
            rb_raise(rb_eTypeError, "element %ld in pixel array is %s, expected %s",
                     x, rb_class2name(CLASS_OF(pixel)), rb_class2name(CLASS_OF(pixel0)));
        }
        if (pixel_class == rb_cFloat)
        {
            pixels.f[x] = (float) NUM2DBL(pixel);
            if (pixels.f[x] < 0.0 || pixels.f[x] > 1.0)
            {
                xfree(pixels.v);
                rb_raise(rb_eArgError, "element %ld is out of range [0..1]: %f", x, pixels.f[x]);
            }
        }
        else
        {
            pixels.i[x] = NUM2QUANTUM(pixel);
        }
    }

    // This is based on ConstituteImage in IM 5.5.7
    new_image = rm_acquire_image((ImageInfo *) NULL);
    if (!new_image)
    {
        xfree(pixels.v);
        rb_raise(rb_eNoMemError, "not enough memory to continue.");
    }

#if defined(IMAGEMAGICK_7)
    exception = AcquireExceptionInfo();
    GVL_STRUCT_TYPE(SetImageExtent) args_SetImageExtent = { new_image, width, height, exception };
    CALL_FUNC_WITHOUT_GVL(GVL_FUNC(SetImageExtent), &args_SetImageExtent);
#else
    GVL_STRUCT_TYPE(SetImageExtent) args_SetImageExtent = { new_image, width, height };
    CALL_FUNC_WITHOUT_GVL(GVL_FUNC(SetImageExtent), &args_SetImageExtent);
    exception = &new_image->exception;
#endif

    if (rm_should_raise_exception(exception, RetainExceptionRetention))
    {
        xfree(pixels.v);
#if defined(IMAGEMAGICK_7)
        DestroyImage(new_image);
        rm_raise_exception(exception);
#else
        rm_check_image_exception(new_image, DestroyOnError);
#endif
    }

#if defined(IMAGEMAGICK_7)
    GVL_STRUCT_TYPE(SetImageBackgroundColor) args_SetImageBackgroundColor = { new_image, exception };
    CALL_FUNC_WITHOUT_GVL(GVL_FUNC(SetImageBackgroundColor), &args_SetImageBackgroundColor);
#else
    GVL_STRUCT_TYPE(SetImageBackgroundColor) args_SetImageBackgroundColor = { new_image };
    CALL_FUNC_WITHOUT_GVL(GVL_FUNC(SetImageBackgroundColor), &args_SetImageBackgroundColor);
    exception = &new_image->exception;
#endif

    if (rm_should_raise_exception(exception, RetainExceptionRetention))
    {
        xfree(pixels.v);
#if defined(IMAGEMAGICK_7)
        DestroyImage(new_image);
        rm_raise_exception(exception);
#else
        rm_check_image_exception(new_image, DestroyOnError);
#endif
    }

#if defined(IMAGEMAGICK_7)
    GVL_STRUCT_TYPE(ImportImagePixels) args_ImportImagePixels = { new_image, 0, 0, width, height, map, stg_type, (const void *)pixels.v, exception };
    CALL_FUNC_WITHOUT_GVL(GVL_FUNC(ImportImagePixels), &args_ImportImagePixels);
    xfree(pixels.v);
    rm_check_exception(exception, new_image, DestroyOnError);
    DestroyExceptionInfo(exception);
#else
    GVL_STRUCT_TYPE(ImportImagePixels) args_ImportImagePixels = { new_image, 0, 0, width, height, map, stg_type, (const void *)pixels.v };
    CALL_FUNC_WITHOUT_GVL(GVL_FUNC(ImportImagePixels), &args_ImportImagePixels);
    xfree(pixels.v);
    rm_check_image_exception(new_image, DestroyOnError);
#endif

    RB_GC_GUARD(pixel);
    RB_GC_GUARD(pixel0);
    RB_GC_GUARD(pixel_class);

    return rm_image_new(new_image);
}

/**
 * Enhance the intensity differences between the lighter and darker elements of the image.
 *
 * @overload contrast(sharpen = false)
 *   @param sharpen [Boolean] If sharpen is true, the contrast is increased, otherwise it is
 *     reduced.
 *   @return [Magick::Image] a new image
 */
VALUE
Image_contrast(int argc, VALUE *argv, VALUE self)
{
    Image *image, *new_image;
    MagickBooleanType sharpen = MagickFalse;
#if defined(IMAGEMAGICK_7)
    ExceptionInfo *exception;
#endif

    image = rm_check_destroyed(self);
    if (argc > 1)
    {
        rb_raise(rb_eArgError, "wrong number of arguments (%d for 0 or 1)", argc);
    }
    else if (argc == 1)
    {
        sharpen = (MagickBooleanType)RTEST(argv[0]);
    }

    new_image = rm_clone_image(image);

#if defined(IMAGEMAGICK_7)
    exception = AcquireExceptionInfo();
    GVL_STRUCT_TYPE(ContrastImage) args = { new_image, sharpen, exception };
    CALL_FUNC_WITHOUT_GVL(GVL_FUNC(ContrastImage), &args);
    rm_check_exception(exception, new_image, DestroyOnError);
    DestroyExceptionInfo(exception);
#else
    GVL_STRUCT_TYPE(ContrastImage) args = { new_image, sharpen };
    CALL_FUNC_WITHOUT_GVL(GVL_FUNC(ContrastImage), &args);
    rm_check_image_exception(new_image, DestroyOnError);
#endif

    return rm_image_new(new_image);
}


/**
 * Convert percentages to #pixels. If the white-point (2nd) argument is not
 * supplied set it to #pixels - black-point.
 *
 * No Ruby usage (internal function)
 *
 * Notes:
 *   - No return value: modifies black_point and white_point directly.
 *
 * @param image the image
 * @param argc number of input arguments
 * @param argv array of input arguments
 * @param black_point pointer to the black point
 * @param white_point pointer to the white point
 */
static void
get_black_white_point(Image *image, int argc, VALUE *argv, double *black_point, double *white_point)
{
    double pixels;

    pixels = (double) (image->columns * image->rows);

    switch (argc)
    {
        case 2:
            if (rm_check_num2dbl(argv[0]))
            {
                *black_point = NUM2DBL(argv[0]);
            }
            else
            {
                *black_point = pixels * rm_str_to_pct(argv[0], true);
            }
            if (rm_check_num2dbl(argv[1]))
            {
                *white_point = NUM2DBL(argv[1]);
            }
            else
            {
                *white_point = pixels * rm_str_to_pct(argv[1], true);
            }
            break;

        case 1:
            if (rm_check_num2dbl(argv[0]))
            {
                *black_point = NUM2DBL(argv[0]);
            }
            else
            {
                *black_point = pixels * rm_str_to_pct(argv[0], true);
            }
            *white_point = pixels - *black_point;
            break;

        default:
            rb_raise(rb_eArgError, "wrong number of arguments (%d for 1 or 2)", argc);
            break;
    }

    return;
}


/**
 * This method is a simple image enhancement technique that attempts to improve the contrast in an
 * image by `stretching' the range of intensity values it contains to span a desired range of
 * values.  It differs from the more sophisticated histogram equalization in that it can only apply
 * a linear scaling function to the image pixel values.
 *
 * @overload contrast_stretch_channel(black_point, white_point = pixels - black_point, channel = Magick::AllChannels)
 *   @param black_point [Numeric, String] black out at most this many pixels. Specify an absolute
 *     number of pixels as a numeric value, or a percentage as a string in the form 'NN%'.
 *   @param white_point [Numeric, String] burn at most this many pixels. Specify an absolute number
 *     of pixels as a numeric value, or a percentage as a string in the form 'NN%'. This argument
 *     is optional. If not specified the default is `(columns * rows) - black_point`.
 *   @param channel [Magick::ChannelType] a ChannelType arguments.
 *
 * @overload contrast_stretch_channel(black_point, white_point = pixels - black_point, *channels)
 *   @param black_point [Numeric, String] black out at most this many pixels. Specify an absolute
 *     number of pixels as a numeric value, or a percentage as a string in the form 'NN%'.
 *   @param white_point [Numeric, String] burn at most this many pixels. Specify an absolute number of
 *     pixels as a numeric value, or a percentage as a string in the form 'NN%'. This argument is
 *     optional. If not specified the default is all pixels - black_point pixels.
 *   @param *channels [Magick::ChannelType] one or more ChannelType arguments.
 *
 * @return [Magick::Image] a new image
 */
VALUE
Image_contrast_stretch_channel(int argc, VALUE *argv, VALUE self)
{
    Image *image, *new_image;
    ChannelType channels;
    double black_point, white_point;
#if defined(IMAGEMAGICK_7)
    ExceptionInfo *exception;
#endif

    image = rm_check_destroyed(self);
    channels = extract_channels(&argc, argv);
    if (argc > 2)
    {
        raise_ChannelType_error(argv[argc-1]);
    }

    get_black_white_point(image, argc, argv, &black_point, &white_point);

    new_image = rm_clone_image(image);

#if defined(IMAGEMAGICK_7)
    exception = AcquireExceptionInfo();
    BEGIN_CHANNEL_MASK(new_image, channels);
    GVL_STRUCT_TYPE(ContrastStretchImage) args = { new_image, black_point, white_point, exception };
    CALL_FUNC_WITHOUT_GVL(GVL_FUNC(ContrastStretchImage), &args);
    END_CHANNEL_MASK(new_image);
    CHECK_EXCEPTION();
    DestroyExceptionInfo(exception);
#else
    GVL_STRUCT_TYPE(ContrastStretchImageChannel) args = { new_image, channels, black_point, white_point };
    CALL_FUNC_WITHOUT_GVL(GVL_FUNC(ContrastStretchImageChannel), &args);
    rm_check_image_exception(new_image, DestroyOnError);
#endif

    return rm_image_new(new_image);
}

/**
 * Apply a user supplied kernel to the image according to the given mophology method.
 *
 * @param method_v [Magick::MorphologyMethod] the morphology method
 * @param iterations [Numeric] apply the operation this many times (or no change).
 *   A value of -1 means loop until no change found.
 *   How this is applied may depend on the morphology method.
 *   Typically this is a value of 1.
 * @param kernel_v [Magick::KernelInfo] morphology kernel to apply
 * @return [Magick::Image] a new image
 */

VALUE
Image_morphology(VALUE self, VALUE method_v, VALUE iterations, VALUE kernel_v)
{
    static VALUE default_channels_const = 0;

    if(!default_channels_const)
    {
        default_channels_const = rb_const_get(Module_Magick, rb_intern("DefaultChannels"));
    }

    return Image_morphology_channel(self, default_channels_const, method_v, iterations, kernel_v);
}

/**
 * Apply a user supplied kernel to the image channel according to the given mophology method.
 *
 * @param channel_v [Magick::ChannelType] a channel type
 * @param method_v [Magick::MorphologyMethod] the morphology method
 * @param iterations_v [Numeric] apply the operation this many times (or no change).
 *   A value of -1 means loop until no change found.
 *   How this is applied may depend on the morphology method.
 *   Typically this is a value of 1.
 * @param kernel_v [Magick::KernelInfo] morphology kernel to apply
 * @return [Magick::Image] a new image
 */

VALUE
Image_morphology_channel(VALUE self, VALUE channel_v, VALUE method_v, VALUE iterations_v, VALUE kernel_v)
{
    Image *image, *new_image;
    ExceptionInfo *exception;
    MorphologyMethod method;
    ChannelType channel;
    KernelInfo *kernel;
    ssize_t iterations = NUM2LONG(iterations_v);;

    image = rm_check_destroyed(self);

    VALUE_TO_ENUM(method_v, method, MorphologyMethod);
    VALUE_TO_ENUM(channel_v, channel, ChannelType);

    if (TYPE(kernel_v) == T_STRING)
    {
        kernel_v = rb_class_new_instance(1, &kernel_v, Class_KernelInfo);
    }

    if (!rb_obj_is_kind_of(kernel_v, Class_KernelInfo))
    {
        rb_raise(rb_eArgError, "expected String or Magick::KernelInfo");
    }

    TypedData_Get_Struct(kernel_v, KernelInfo, &rm_kernel_info_data_type, kernel);

    exception = AcquireExceptionInfo();

#if defined(IMAGEMAGICK_7)
    BEGIN_CHANNEL_MASK(image, channel);
    GVL_STRUCT_TYPE(MorphologyImage) args = { image, method, iterations, kernel, exception };
    new_image = (Image *)CALL_FUNC_WITHOUT_GVL(GVL_FUNC(MorphologyImage), &args);
    CHANGE_RESULT_CHANNEL_MASK(new_image);
    END_CHANNEL_MASK(image);
#else
    GVL_STRUCT_TYPE(MorphologyImageChannel) args = { image, channel, method, iterations, kernel, exception };
    new_image = (Image *)CALL_FUNC_WITHOUT_GVL(GVL_FUNC(MorphologyImageChannel), &args);
#endif
    rm_check_exception(exception, new_image, DestroyOnError);
    DestroyExceptionInfo(exception);

    return rm_image_new(new_image);
}

#if defined(IMAGEMAGICK_7)
// TODO: Move this to KernelInfo class as a constructor?
KernelInfo*
convolve_create_kernel_info(unsigned int order, VALUE kernel_arg)
{
    unsigned int x;
    KernelInfo *kernel;
    ExceptionInfo *exception;

    exception = AcquireExceptionInfo();
    kernel = AcquireKernelInfo((const char *) NULL, exception);
    CHECK_EXCEPTION();
    DestroyExceptionInfo(exception);
    if (!kernel)
    {
        rb_raise(rb_eNoMemError, "not enough memory to initialize KernelInfo");
    }

    kernel->width = order;
    kernel->height = order;
    kernel->x = (ssize_t)(order - 1) / 2;
    kernel->y = (ssize_t)(order - 1) / 2;
    kernel->values = (MagickRealType *) AcquireAlignedMemory(order, order*sizeof(*kernel->values));
    if (!kernel->values)
    {
        DestroyKernelInfo(kernel);
        rb_raise(rb_eNoMemError, "not enough memory to initialize KernelInfo values");
    }

    for (x = 0; x < order*order; x++)
    {
        VALUE element = rb_ary_entry(kernel_arg, (long)x);
        if (rm_check_num2dbl(element))
        {
            kernel->values[x] = NUM2DBL(element);
        }
        else
        {
            DestroyKernelInfo(kernel);
            rb_raise(rb_eTypeError, "type mismatch: %s given", rb_class2name(CLASS_OF(element)));
        }
    }

    return kernel;
}
#endif

/**
 * Apply a custom convolution kernel to the image.
 *
 * @param order_arg [Numeric] the number of rows and columns in the kernel
 * @param kernel_arg [Array<Float>] An `order*order` matrix of {Float} values.
 * @return [Magick::Image] a new image
 */
VALUE
Image_convolve(VALUE self, VALUE order_arg, VALUE kernel_arg)
{
    Image *image, *new_image;
    size_t order;
    ExceptionInfo *exception;
#if defined(IMAGEMAGICK_7)
    KernelInfo *kernel;
#else
    double *kernel;
    unsigned int x;
#endif

    image = rm_check_destroyed(self);

    if (NUM2INT(order_arg) <= 0)
    {
        rb_raise(rb_eArgError, "order must be non-zero and positive");
    }

    order = NUM2INT(order_arg);
    kernel_arg = rb_Array(kernel_arg);
    rm_check_ary_len(kernel_arg, (long)(order*order));

#if defined(IMAGEMAGICK_7)
    kernel = convolve_create_kernel_info(order, kernel_arg);
#else
    // Convert the kernel array argument to an array of doubles

    kernel = (double *)ALLOC_N(double, order*order);
    for (x = 0; x < (unsigned)(order * order); x++)
    {
        VALUE element = rb_ary_entry(kernel_arg, (long)x);
        if (rm_check_num2dbl(element))
        {
            kernel[x] = NUM2DBL(element);
        }
        else
        {
            xfree((void *)kernel);
            rb_raise(rb_eTypeError, "type mismatch: %s given", rb_class2name(CLASS_OF(element)));
        }
    }
#endif

    exception = AcquireExceptionInfo();

#if defined(IMAGEMAGICK_7)
    GVL_STRUCT_TYPE(ConvolveImage) args = { image, kernel, exception };
    new_image = (Image *)CALL_FUNC_WITHOUT_GVL(GVL_FUNC(ConvolveImage), &args);
    DestroyKernelInfo(kernel);
#else
    GVL_STRUCT_TYPE(ConvolveImage) args = { image, order, kernel, exception };
    new_image = (Image *)CALL_FUNC_WITHOUT_GVL(GVL_FUNC(ConvolveImage), &args);
    xfree((void *)kernel);
#endif

    rm_check_exception(exception, new_image, DestroyOnError);
    DestroyExceptionInfo(exception);

    return rm_image_new(new_image);
}


/**
 * Applies a custom convolution kernel to the specified channel or channels in the image.
 *
 * @overload convolve_channel(order, kernel, channel = Magick::AllChannels)
 *   @param order_arg [Numeric] the number of rows and columns in the kernel
 *   @param kernel_arg [Array<Float>] An `order*order` matrix of {Float} values.
 *   @param channel [Magick::ChannelType] a ChannelType arguments.
 *
 * @overload convolve_channel(order, kernel, *channels)
 *   @param order_arg [Numeric] the number of rows and columns in the kernel
 *   @param kernel_arg [Array<Float>] An `order*order` matrix of {Float} values.
 *   @param *channels [Magick::ChannelType] one or more ChannelType arguments.
 *
 * @return [Magick::Image] a new image
 */
VALUE
Image_convolve_channel(int argc, VALUE *argv, VALUE self)
{
    Image *image, *new_image;
    VALUE ary;
    size_t order;
    ChannelType channels;
    ExceptionInfo *exception;
#if defined(IMAGEMAGICK_7)
    KernelInfo *kernel;
#else
    double *kernel;
    unsigned int x;
#endif

    image = rm_check_destroyed(self);

    channels = extract_channels(&argc, argv);

    // There are 2 required arguments.
    if (argc > 2)
    {
        raise_ChannelType_error(argv[argc-1]);
    }
    if (argc != 2)
    {
        rb_raise(rb_eArgError, "wrong number of arguments (%d for 2 or more)", argc);
    }

    if (NUM2INT(argv[0]) <= 0)
    {
        rb_raise(rb_eArgError, "order must be non-zero and positive");
    }

    order = NUM2INT(argv[0]);
    ary = rb_Array(argv[1]);
    rm_check_ary_len(ary, (long)(order*order));

#if defined(IMAGEMAGICK_7)
    kernel = convolve_create_kernel_info(order, ary);
#else
    kernel = ALLOC_N(double, (long)(order*order));

    // Convert the kernel array argument to an array of doubles
    for (x = 0; x < (unsigned)(order * order); x++)
    {
        VALUE element = rb_ary_entry(ary, (long)x);
        if (rm_check_num2dbl(element))
        {
            kernel[x] = NUM2DBL(element);
        }
        else
        {
            xfree((void *)kernel);
            rb_raise(rb_eTypeError, "type mismatch: %s given", rb_class2name(CLASS_OF(element)));
        }
    }
#endif

    exception = AcquireExceptionInfo();

#if defined(IMAGEMAGICK_7)
    BEGIN_CHANNEL_MASK(image, channels);
    GVL_STRUCT_TYPE(ConvolveImage) args = { image, kernel, exception };
    new_image = (Image *)CALL_FUNC_WITHOUT_GVL(GVL_FUNC(ConvolveImage), &args);
    CHANGE_RESULT_CHANNEL_MASK(new_image);
    END_CHANNEL_MASK(image);
    DestroyKernelInfo(kernel);
#else
    GVL_STRUCT_TYPE(ConvolveImageChannel) args = { image, channels, order, kernel, exception };
    new_image = (Image *)CALL_FUNC_WITHOUT_GVL(GVL_FUNC(ConvolveImageChannel), &args);
    xfree((void *)kernel);
#endif

    rm_check_exception(exception, new_image, DestroyOnError);
    DestroyExceptionInfo(exception);

    RB_GC_GUARD(ary);

    return rm_image_new(new_image);
}



/**
 * Alias for {Magick::Image#dup}.
 *
 * @return [Magick::Image] a new image
 */
VALUE
Image_copy(VALUE self)
{
    return rb_funcall(self, rm_ID_dup, 0);
}

/**
 * Initialize copy, clone, dup.
 *
 * @param orig [Magick::Image] the source image
 * @return [Magick::Image] self
 * @see Image#copy
 * @see Image#clone
 * @see Image#dup
 */
VALUE
Image_init_copy(VALUE copy, VALUE orig)
{
    Image *image, *new_image;

    image = rm_check_destroyed(orig);
    new_image = rm_clone_image(image);
    UPDATE_DATA_PTR(copy, new_image);

    return copy;
}


/**
 * Extract a region of the image defined by width, height, x, y.
 *
 * @overload crop(x, y, width, height, reset = false)
 *   @param x [Numeric] x position of start of region
 *   @param y [Numeric] y position of start of region
 *   @param width [Numeric] width of region
 *   @param height [Numeric] height of region
 *   @param reset [Boolean] true if reset the cropped image page canvas and position
 *
 * @overload crop(gravity, width, height, reset = false)
 *   @param gravity [Magick::GravityType] the gravity type
 *   @param width [Numeric] width of region
 *   @param height [Numeric] height of region
 *   @param reset [Boolean] true if reset the cropped image page canvas and position

 * @overload crop(gravity, x, y, width, height, reset = false)
 *   @param gravity [Magick::GravityType] the gravity type
 *   @param x [Numeric] x position of start of region
 *   @param y [Numeric] y position of start of region
 *   @param width [Numeric] width of region
 *   @param height [Numeric] height of region
 *   @param reset [Boolean] true if reset the cropped image page canvas and position
 *
 * @return [Magick::Image] a new image
 * @see Image#crop!
 */
VALUE
Image_crop(int argc, VALUE *argv, VALUE self)
{
    rm_check_destroyed(self);
    return cropper(False, argc, argv, self);
}


/**
 * Extract a region of the image defined by width, height, x, y.
 * In-place form of {Image#crop}.
 *
 * @overload crop!(x, y, width, height, reset = false)
 *   @param x [Numeric] x position of start of region
 *   @param y [Numeric] y position of start of region
 *   @param width [Numeric] width of region
 *   @param height [Numeric] height of region
 *   @param reset [Boolean] true if reset the cropped image page canvas and position
 *
 * @overload crop!(gravity, width, height, reset = false)
 *   @param gravity [Magick::GravityType] the gravity type
 *   @param width [Numeric] width of region
 *   @param height [Numeric] height of region
 *   @param reset [Boolean] true if reset the cropped image page canvas and position

 * @overload crop!(gravity, x, y, width, height, reset = false)
 *   @param gravity [Magick::GravityType] the gravity type
 *   @param x [Numeric] x position of start of region
 *   @param y [Numeric] y position of start of region
 *   @param width [Numeric] width of region
 *   @param height [Numeric] height of region
 *   @param reset [Boolean] true if reset the cropped image page canvas and position
 *
 * @return [Magick::Image] a new image
 * @see Image#crop!
 */
VALUE
Image_crop_bang(int argc, VALUE *argv, VALUE self)
{
    rm_check_frozen(self);
    return cropper(True, argc, argv, self);
}


/**
 * Displaces the colormap by a given number of positions.
 * If you cycle the colormap a number of times you can produce a psychedelic effect.
 *
 * The returned image is always a PseudoClass image, regardless of the type of the original image.
 *
 * @param amount [Numeric] amount to cycle the colormap
 * @return [Magick::Image] a new image
 */
VALUE
Image_cycle_colormap(VALUE self, VALUE amount)
{
    Image *image, *new_image;
    int amt;
#if defined(IMAGEMAGICK_7)
    ExceptionInfo *exception;
#endif

    amt = NUM2INT(amount);

    image = rm_check_destroyed(self);
    new_image = rm_clone_image(image);

#if defined(IMAGEMAGICK_7)
    exception = AcquireExceptionInfo();
    GVL_STRUCT_TYPE(CycleColormapImage) args = { new_image, amt, exception };
    CALL_FUNC_WITHOUT_GVL(GVL_FUNC(CycleColormapImage), &args);
    rm_check_exception(exception, new_image, DestroyOnError);
    DestroyExceptionInfo(exception);
#else
    GVL_STRUCT_TYPE(CycleColormapImage) args = { new_image, amt };
    CALL_FUNC_WITHOUT_GVL(GVL_FUNC(CycleColormapImage), &args);
    rm_check_image_exception(new_image, DestroyOnError);
#endif

    return rm_image_new(new_image);
}


/**
 * Get the vertical and horizontal resolution in pixels of the image.
 * The default is "72x72".
 *
 * @return [String] a string of geometry in the form "XresxYres"
 * @see https://www.imagemagick.org/Magick++/Geometry.html
 */
VALUE
Image_density(VALUE self)
{
    Image *image;
    char density[128];

    image = rm_check_destroyed(self);

#if defined(IMAGEMAGICK_7)
    snprintf(density, sizeof(density), "%gx%g", image->resolution.x, image->resolution.y);
#else
    snprintf(density, sizeof(density), "%gx%g", image->x_resolution, image->y_resolution);
#endif
    return rb_str_new2(density);
}


/**
 * Set the vertical and horizontal resolution in pixels of the image.
 *
 * - The density is a string of the form "XresxYres" or simply "Xres".
 * - If the y resolution is not specified, set it equal to the x resolution.
 * - This is equivalent to PerlMagick's handling of density.
 * - The density can also be a Geometry object. The width attribute is used for the x
 *   resolution. The height attribute is used for the y resolution.  If the height attribute is
 *   missing, the width attribute is used for both.
 *
 * @param density_arg [Magick::Geometry, String] The density String or Geometry
 * @return [Magick::Geometry, String] the given value
 * @see https://www.imagemagick.org/Magick++/Geometry.html
 */
VALUE
Image_density_eq(VALUE self, VALUE density_arg)
{
    Image *image;
    char *density;
    VALUE x_val, y_val;
    int count;
    double x_res, y_res;

    image = rm_check_frozen(self);

    // Get the Class ID for the Geometry class.
    if (!Class_Geometry)
    {
        Class_Geometry = rb_const_get(Module_Magick, rm_ID_Geometry);
    }

    // Geometry object. Width and height attributes are always positive.
    if (CLASS_OF(density_arg) == Class_Geometry)
    {
        x_val = rb_funcall(density_arg, rm_ID_width, 0);
        x_res = NUM2DBL(x_val);
        y_val = rb_funcall(density_arg, rm_ID_height, 0);
        y_res = NUM2DBL(y_val);
        if (x_res == 0.0)
        {
            rb_raise(rb_eArgError, "invalid x resolution: %f", x_res);
        }
#if defined(IMAGEMAGICK_7)
        image->resolution.y = y_res != 0.0 ? y_res : x_res;
        image->resolution.x = x_res;
#else
        image->y_resolution = y_res != 0.0 ? y_res : x_res;
        image->x_resolution = x_res;
#endif
    }

    // Convert the argument to a string
    else
    {
        density = StringValueCStr(density_arg);
        if (!IsGeometry(density))
        {
            rb_raise(rb_eArgError, "invalid density geometry %s", density);
        }

#if defined(IMAGEMAGICK_7)
        count = sscanf(density, "%lfx%lf", &image->resolution.x, &image->resolution.y);
#else
        count = sscanf(density, "%lfx%lf", &image->x_resolution, &image->y_resolution);
#endif
        if (count < 2)
        {
#if defined(IMAGEMAGICK_7)
            image->resolution.y = image->resolution.x;
#else
            image->y_resolution = image->x_resolution;
#endif
        }

    }

    RB_GC_GUARD(x_val);
    RB_GC_GUARD(y_val);

    return density_arg;
}


/**
 * Decipher an enciphered image.
 *
 * @param passphrase [String] The passphrase used to encipher the image.
 * @return [Magick::Image] a new deciphered image
 */
VALUE
Image_decipher(VALUE self, VALUE passphrase)
{
    Image *image, *new_image;
    char *pf;
    ExceptionInfo *exception;
    MagickBooleanType okay;

    image = rm_check_destroyed(self);
    pf = StringValueCStr(passphrase);      // ensure passphrase is a string
    exception = AcquireExceptionInfo();

    new_image = rm_clone_image(image);

    GVL_STRUCT_TYPE(DecipherImage) args = { new_image, pf, exception };
    void *ret = CALL_FUNC_WITHOUT_GVL(GVL_FUNC(DecipherImage), &args);
    okay = static_cast<MagickBooleanType>(reinterpret_cast<intptr_t &>(ret));
    rm_check_exception(exception, new_image, DestroyOnError);
    if (!okay)
    {
        DestroyImage(new_image);
        rb_raise(rb_eRuntimeError, "DecipherImage failed for unknown reason.");
    }

    DestroyExceptionInfo(exception);

    return rm_image_new(new_image);
}


/**
 * Associates makes a copy of the given string arguments and
 * inserts it into the artifact tree.
 *
 * - Normally a script should never call this method. Any calls to
 *   SetImageArtifact will be part of the methods in which they're needed, or
 *   be called via the OptionalMethodArguments class.
 * - If value is nil, the artifact will be removed
 *
 * @param artifact [String] the artifact to set
 * @param value [String, nil] the value to which to set the artifact
 * @return [String, nil] the given `value`
 */
VALUE
Image_define(VALUE self, VALUE artifact, VALUE value)
{
    Image *image;
    char *key, *val;
    MagickBooleanType status;

    image = rm_check_frozen(self);
    artifact = rb_String(artifact);
    key = StringValueCStr(artifact);

    if (value == Qnil)
    {
        DeleteImageArtifact(image, key);
    }
    else
    {
        value = rb_String(value);
        val = StringValueCStr(value);
        status = SetImageArtifact(image, key, val);
        if (!status)
        {
            rb_raise(rb_eNoMemError, "not enough memory to continue");
        }
    }

    return value;
}


/**
 * Get the Number of ticks which must expire before displaying the next image in an animated
 * sequence. The default number of ticks is 0. By default there are 100 ticks per second but this
 * number can be changed via the ticks_per_second attribute.
 *
 * @return [Integer] The current delay value.
 */
VALUE
Image_delay(VALUE self)
{
    IMPLEMENT_TYPED_ATTR_READER(Image, delay, ulong, &rm_image_data_type);
}

/**
 * Set the Number of ticks which must expire before displaying the next image in an animated
 * sequence.
 *
 * @param val [Numeric] the delay value
 * @return [Numeric] the given value
 */
VALUE
Image_delay_eq(VALUE self, VALUE val)
{
    IMPLEMENT_TYPED_ATTR_WRITER(Image, delay, ulong, &rm_image_data_type);
}


/**
 * Delete the image composite mask.
 *
 * @return [Magick::Image] self
 * @see Image#add_compose_mask
 */
VALUE
Image_delete_compose_mask(VALUE self)
{
    Image *image;
#if defined(IMAGEMAGICK_7)
    ExceptionInfo *exception;
#endif

    image = rm_check_frozen(self);

#if defined(IMAGEMAGICK_7)
    exception = AcquireExceptionInfo();
    GVL_STRUCT_TYPE(SetImageMask) args = { image, CompositePixelMask, NULL, exception };
    CALL_FUNC_WITHOUT_GVL(GVL_FUNC(SetImageMask), &args);
    CHECK_EXCEPTION();
    DestroyExceptionInfo(exception);
#else
    GVL_STRUCT_TYPE(SetImageMask) args = { image, NULL };
    CALL_FUNC_WITHOUT_GVL(GVL_FUNC(SetImageMask), &args);
    rm_check_image_exception(image, RetainOnError);
#endif

    return self;
}


/**
 * Deletes the specified profile.
 *
 * @param name [String] The profile name, "IPTC" or "ICC" for example.
 *   Specify "*" to delete all the profiles in the image.
 * @return [Magick::Image] self
 * @see Image#add_profile
 */
VALUE
Image_delete_profile(VALUE self, VALUE name)
{
    Image *image = rm_check_frozen(self);
    char *profile_name = StringValueCStr(name);

#if defined(IMAGEMAGICK_7)
    ExceptionInfo *exception = AcquireExceptionInfo();

    GVL_STRUCT_TYPE(ProfileImage) args = { image, profile_name, NULL, 0, exception };
    CALL_FUNC_WITHOUT_GVL(GVL_FUNC(ProfileImage), &args);
    CHECK_EXCEPTION();
    DestroyExceptionInfo(exception);
#else
    GVL_STRUCT_TYPE(ProfileImage) args = { image, profile_name, NULL, 0, MagickTrue };
    CALL_FUNC_WITHOUT_GVL(GVL_FUNC(ProfileImage), &args);
#endif
    return self;
}


/**
 * Return the image depth (8, 16 or 32).
 *
 * - If all pixels have lower-order bytes equal to higher-order bytes, the depth will be reported as
 *   8 even if the depth field in the Image structure says 16.
 *
 * @return [Integer] the depth
 */
VALUE
Image_depth(VALUE self)
{
    Image *image;
    size_t depth = 0;
    ExceptionInfo *exception;

    image = rm_check_destroyed(self);
    exception = AcquireExceptionInfo();

    GVL_STRUCT_TYPE(GetImageDepth) args = { image, exception };
    depth = (size_t)CALL_FUNC_WITHOUT_GVL(GVL_FUNC(GetImageDepth), &args);
    CHECK_EXCEPTION();

    DestroyExceptionInfo(exception);

    return INT2FIX(depth);
}


/**
 * Straightens an image. A threshold of 40% works for most images.
 *
 * @overload deskew(threshold = 0.40, auto_crop_width = nil)
 *   @param threshold [Numeric, String] A percentage of QuantumRange. Either a Float between 0 and 1.0,
 *     inclusive, or a string in the form "NN%" where NN is between 0 and 100.
 *   @param auto_crop_width [Numeric] Specify a value for this argument to cause the deskewed image to
 *     be auto-cropped. The argument is the pixel width of the image background (e.g. 40).
 *   @return [Magick::Image] a new image
 */
VALUE
Image_deskew(int argc, VALUE *argv, VALUE self)
{
    Image *image, *new_image;
    double threshold = 40.0 * QuantumRange / 100.0;
    unsigned long width;
    char auto_crop_width[20];
    ExceptionInfo *exception;

    image = rm_check_destroyed(self);

    switch (argc)
    {
        case 2:
            width = NUM2ULONG(argv[1]);
            memset(auto_crop_width, 0, sizeof(auto_crop_width));
            snprintf(auto_crop_width, sizeof(auto_crop_width), "%lu", width);
            SetImageArtifact(image, "deskew:auto-crop", auto_crop_width);
        case 1:
            threshold = rm_percentage(argv[0], 1.0) * QuantumRange;
        case 0:
            break;
        default:
            rb_raise(rb_eArgError, "wrong number of arguments (%d for 0 to 2)", argc);
            break;
    }

    exception = AcquireExceptionInfo();
    GVL_STRUCT_TYPE(DeskewImage) args = { image, threshold, exception };
    new_image = (Image *)CALL_FUNC_WITHOUT_GVL(GVL_FUNC(DeskewImage), &args);
    CHECK_EXCEPTION();
    DestroyExceptionInfo(exception);

    return rm_image_new(new_image);
}


/**
 * Reduce the speckle noise in an image while preserving the edges of the original image.
 *
 * @return [Magick::Image] a new image
 */
VALUE
Image_despeckle(VALUE self)
{
    Image *image, *new_image;
    ExceptionInfo *exception;

    image = rm_check_destroyed(self);
    exception = AcquireExceptionInfo();

    GVL_STRUCT_TYPE(DespeckleImage) args = { image, exception };
    new_image = (Image *)CALL_FUNC_WITHOUT_GVL(GVL_FUNC(DespeckleImage), &args);
    rm_check_exception(exception, new_image, DestroyOnError);
    DestroyExceptionInfo(exception);

    return rm_image_new(new_image);
}


/**
 * Free all the memory associated with an image.
 *
 * @return [Magick::Image] self
 */
VALUE
Image_destroy_bang(VALUE self)
{
    Image *image;

    rb_check_frozen(self);
    TypedData_Get_Struct(self, Image, &rm_image_data_type, image);
    rm_image_destroy(image);
    DATA_PTR(self) = NULL;
    return self;
}


/**
 * Return true if the image has been destroyed, false otherwise.
 *
 * @return [Boolean] true if destroyed, false otherwise
 */
VALUE
Image_destroyed_q(VALUE self)
{
    Image *image;

    TypedData_Get_Struct(self, Image, &rm_image_data_type, image);
    return image ? Qfalse : Qtrue;
}


/**
 * Compares two images and computes statistics about their difference.
 *
 * @param other [Magick::Image, Magick::ImageList] Either an imagelist or an image. If an imagelist,
 *   uses the current image.
 * @return [Array<Float>] An array of three {Float} values:
 *   - mean error per pixel
 *     - The mean error for any single pixel in the image.
 *   - normalized mean error
 *     - The normalized mean quantization error for any single pixel in the image. This distance measure
 *       is normalized to a range between 0 and 1. It is independent of the range of red, green, and
 *       blue values in the image.
 *   - normalized maximum error
 *     - The normalized maximum quantization error for any single pixel in the image. This distance
 *       measure is normalized to a range between 0 and 1. It is independent of the range of red,
 *       green, and blue values in your image.
 */
VALUE
Image_difference(VALUE self, VALUE other)
{
    Image *image;
    Image *image2;
    VALUE mean, nmean, nmax;
#if defined(IMAGEMAGICK_7)
    double distortion;
    ExceptionInfo *exception;
#endif

    image = rm_check_destroyed(self);
    other = rm_cur_image(other);
    image2 = rm_check_destroyed(other);

#if defined(IMAGEMAGICK_7)
    exception = AcquireExceptionInfo();
    GVL_STRUCT_TYPE(GetImageDistortion) args = { image, image2, MeanErrorPerPixelErrorMetric, &distortion, exception };
    CALL_FUNC_WITHOUT_GVL(GVL_FUNC(GetImageDistortion), &args);
    CHECK_EXCEPTION();
    DestroyExceptionInfo(exception);
#else
    GVL_STRUCT_TYPE(IsImagesEqual) args = { image, image2 };
    CALL_FUNC_WITHOUT_GVL(GVL_FUNC(IsImagesEqual), &args);
    rm_check_image_exception(image, RetainOnError);
#endif

    mean  = rb_float_new(image->error.mean_error_per_pixel);
    nmean = rb_float_new(image->error.normalized_mean_error);
    nmax  = rb_float_new(image->error.normalized_maximum_error);

    RB_GC_GUARD(mean);
    RB_GC_GUARD(nmean);
    RB_GC_GUARD(nmax);

    return rb_ary_new3(3, mean, nmean, nmax);
}


/**
 * Get image directory.
 *
 * @return [String, nil] the directory
 */
VALUE
Image_directory(VALUE self)
{
    IMPLEMENT_TYPED_ATTR_READER(Image, directory, str, &rm_image_data_type);
}


/**
 * Uses displacement_map to move color from img to the output image.
 * This method corresponds to the -displace option of ImageMagick's composite command.
 *
 * @overload displace(displacement_map, x_amp, y_amp = x_amp, gravity = Magick::NorthWestGravity, x_offset = 0, y_offset = 0)
 *   @param displacement_map [Magick::Image, Magick::ImageList] The source image for the composite
 *     operation. Either an imagelist or an image. If an imagelist, uses the current image.
 *   @param x_amp [Numeric] The maximum displacement on the x-axis.
 *   @param y_amp [Numeric] The maximum displacement on the y-axis.
 *   @param gravity [Magick::GravityType] the gravity for offset. the offsets are measured from the
 *   NorthWest corner by default.
 *   @param x_offset [Numeric] The offset that measured from the left-hand side of the target image.
 *   @param y_offset [Numeric] The offset that measured from the top of the target image.
 *   @return [Magick::Image] a new image
 */
VALUE
Image_displace(int argc, VALUE *argv, VALUE self)
{
    Image *image, *displacement_map;
    VALUE dmap;
    double x_amplitude = 0.0, y_amplitude = 0.0;
    long x_offset = 0L, y_offset = 0L;

    image = rm_check_destroyed(self);

    if (argc < 2)
    {
        rb_raise(rb_eArgError, "wrong number of arguments (%d for 2 to 6)", argc);
    }

    dmap = rm_cur_image(argv[0]);
    displacement_map = rm_check_destroyed(dmap);

    if (argc > 3)
    {
        get_composite_offsets(argc-3, &argv[3], image, displacement_map, &x_offset, &y_offset);
        // There must be 3 arguments left
        argc = 3;
    }

    switch (argc)
    {
        case 3:
            y_amplitude = NUM2DBL(argv[2]);
            x_amplitude = NUM2DBL(argv[1]);
            break;
        case 2:
            x_amplitude = NUM2DBL(argv[1]);
            y_amplitude = x_amplitude;
            break;
    }

    RB_GC_GUARD(dmap);

    return special_composite(image, displacement_map, x_amplitude, y_amplitude,
                             x_offset, y_offset, DisplaceCompositeOp);
}


/**
 * Extract pixel data from the image and returns it as an array of pixels. The "x", "y", "width" and
 * "height" parameters specify the rectangle to be extracted. The "map" parameter reflects the
 * expected ordering of the pixel array. It can be any combination or order of R = red, G = green,
 * B = blue, A = alpha, C = cyan, Y = yellow, M = magenta, K = black, or I = intensity (for
 * grayscale). If the "float" parameter is specified and true, the pixel data is returned as
 * floating-point numbers in the range [0..1]. By default the pixel data is returned as integers in
 * the range [0..QuantumRange].
 *
 * @overload dispatch(x, y, columns, rows, map, float = false)
 *   @param x [Numeric] The offset of the rectangle from the upper-left corner of the image.
 *   @param y [Numeric] The offset of the rectangle from the upper-left corner of the image.
 *   @param columns [Numeric] The width of the rectangle.
 *   @param rows [Numeric] The height of the rectangle.
 *   @param map [String]
 *   @param float [Boolean]
 *   @return [Array<Numeric>] an Array of pixel data
 */
VALUE
Image_dispatch(int argc, VALUE *argv, VALUE self)
{
    Image *image;
    long x, y;
    unsigned long columns, rows, n, npixels;
    VALUE pixels_ary;
    StorageType stg_type = QuantumPixel;
    char *map;
    size_t mapL;
    MagickBooleanType okay;
    ExceptionInfo *exception;
    volatile union
    {
        Quantum *i;
        double *f;
        void *v;
    } pixels;

    rm_check_destroyed(self);

    if (argc < 5 || argc > 6)
    {
        rb_raise(rb_eArgError, "wrong number of arguments (%d for 5 or 6)", argc);
    }

    x       = NUM2LONG(argv[0]);
    y       = NUM2LONG(argv[1]);
    columns = NUM2ULONG(argv[2]);
    rows    = NUM2ULONG(argv[3]);
    map     = rm_str2cstr(argv[4], &mapL);
    if (argc == 6)
    {
        stg_type = RTEST(argv[5]) ? DoublePixel : QuantumPixel;
    }

    // Compute the size of the pixel array and allocate the memory.
    npixels = columns * rows * mapL;
    pixels.v = stg_type == QuantumPixel ? (void *) ALLOC_N(Quantum, npixels)
               : (void *) ALLOC_N(double, npixels);

    // Create the Ruby array for the pixels. Return this even if ExportImagePixels fails.
    pixels_ary = rb_ary_new();

    TypedData_Get_Struct(self, Image, &rm_image_data_type, image);

    exception = AcquireExceptionInfo();
    GVL_STRUCT_TYPE(ExportImagePixels) args = { image, x, y, columns, rows, map, stg_type, (void *)pixels.v, exception };
    void *ret = CALL_FUNC_WITHOUT_GVL(GVL_FUNC(ExportImagePixels), &args);
    okay = static_cast<MagickBooleanType>(reinterpret_cast<intptr_t &>(ret));

    if (!okay)
    {
        goto exit;
    }

    CHECK_EXCEPTION();

    DestroyExceptionInfo(exception);

    // Convert the pixel data to the appropriate Ruby type
    if (stg_type == QuantumPixel)
    {
        for (n = 0; n < npixels; n++)
        {
            rb_ary_push(pixels_ary, QUANTUM2NUM(pixels.i[n]));
        }
    }
    else
    {
        for (n = 0; n < npixels; n++)
        {
            rb_ary_push(pixels_ary, rb_float_new(pixels.f[n]));
        }
    }

    exit:
    xfree((void *)pixels.v);

    RB_GC_GUARD(pixels_ary);

    return pixels_ary;
}


/**
 * Display the image to an X window screen.
 *
 * @yield [info]
 * @yieldparam info [Magick::Image::Info]
 * @return [Magick::Image] self
 */
VALUE
Image_display(VALUE self)
{
    Image *image;
    Info *info;
    VALUE info_obj;
#if defined(IMAGEMAGICK_7)
    ExceptionInfo *exception;
#endif

    image = rm_check_destroyed(self);

    if (image->rows == 0 || image->columns == 0)
    {
        rb_raise(rb_eArgError, "invalid image geometry (%" RMIuSIZE "x%" RMIuSIZE ")", image->rows, image->columns);
    }

    info_obj = rm_info_new();
    TypedData_Get_Struct(info_obj, Info, &rm_info_data_type, info);

#if defined(IMAGEMAGICK_7)
    exception = AcquireExceptionInfo();
    DisplayImages(info, image, exception);
    CHECK_EXCEPTION();
    DestroyExceptionInfo(exception);
#else
    DisplayImages(info, image);
    rm_check_image_exception(image, RetainOnError);
#endif

    RB_GC_GUARD(info_obj);

    return self;
}


/**
 * Return the dispose attribute as a DisposeType enum.
 *
 * @return [Magick::DisposeType] the dispose
 */
VALUE
Image_dispose(VALUE self)
{
    Image *image = rm_check_destroyed(self);
    return DisposeType_find(image->dispose);
}


/**
 * Set the dispose attribute.
 *
 * @param dispose [Magick::DisposeType] the dispose
 * @return [Magick::DisposeType] the given dispose
 */
VALUE
Image_dispose_eq(VALUE self, VALUE dispose)
{
    Image *image = rm_check_frozen(self);
    VALUE_TO_ENUM(dispose, image->dispose, DisposeType);
    return dispose;
}


/**
 * Composites the overlay image into the target image.
 * The opacity of img is multiplied by dst_percentage and opacity of overlay is multiplied by
 * src_percentage.
 *
 * This method corresponds to the -dissolve option of ImageMagick's composite command.
 *
 * @overload dissolve(overlay, src_percent, dst_percent = -1.0, gravity = Magick::NorthWestGravity, x_offset = 0, y_offset = 0)
 *   @param overlay [Magick::Image, Magick::ImageList] The source image for the composite operation.
 *     Either an imagelist or an image. If an imagelist, uses the current image.
 *   @param src_percent [Numeric, String] Either a non-negative number a string in the form "NN%".
 *     If src_percentage is a number it is interpreted as a percentage.
 *     Both 0.25 and "25%" mean 25%. This argument is required.
 *   @param dst_percent [Numeric, String] Either a non-negative number a string in the form "NN%".
 *     If src_percentage is a number it is interpreted as a percentage.
 *     Both 0.25 and "25%" mean 25%. This argument may omitted if no other arguments follow it.
 *     In this case the default is 100%-src_percentage.
 *   @param gravity [Magick::GravityType] the gravity for offset. the offsets are measured from the
 *     NorthWest corner by default.
 *   @param x_offset [Numeric] The offset that measured from the left-hand side of the target image.
 *   @param y_offset [Numeric] The offset that measured from the top of the target image.
 *   @return [Magick::Image] a new image
 */
VALUE
Image_dissolve(int argc, VALUE *argv, VALUE self)
{
    Image *image, *overlay;
    double src_percent, dst_percent = -1.0;
    long x_offset = 0L, y_offset = 0L;
    VALUE composite_image, ovly;

    image = rm_check_destroyed(self);

    if (argc < 1)
    {
        rb_raise(rb_eArgError, "wrong number of arguments (%d for 2 to 6)", argc);
    }

    ovly = rm_cur_image(argv[0]);
    overlay = rm_check_destroyed(ovly);

    if (argc > 3)
    {
        get_composite_offsets(argc-3, &argv[3], image, overlay, &x_offset, &y_offset);
        // There must be 3 arguments left
        argc = 3;
    }

    switch (argc)
    {
        case 3:
            dst_percent = rm_percentage(argv[2], 1.0) * 100.0;
        case 2:
            src_percent = rm_percentage(argv[1], 1.0) * 100.0;
            break;
        default:
            rb_raise(rb_eArgError, "wrong number of arguments (%d for 2 to 6)", argc);
            break;
    }

    composite_image =  special_composite(image, overlay, src_percent, dst_percent,
                                         x_offset, y_offset, DissolveCompositeOp);

    RB_GC_GUARD(composite_image);
    RB_GC_GUARD(ovly);

    return composite_image;
}


/**
 * Distort an image using the specified distortion type and its required arguments.
 * This method is equivalent to ImageMagick's -distort option.
 *
 * @overload distort(type, points, bestfit = false)
 *   @param type [Magick::DistortMethod] a DistortMethod value
 *   @param points [Array<Numeric>] an Array of Numeric values. The size of the array depends on the
 *     distortion type.
 *   @param bestfit [Boolean] If bestfit is enabled, and the distortion allows it, the destination
 *     image is adjusted to ensure the whole source image will just fit within the final destination
 *     image, which will be sized and offset accordingly.  Also in many cases the virtual offset of
 *     the source image will be taken into account in the mapping.
 *
 * @overload distort(type, points, bestfit = false)
 *   When a block is given, distort yields with a block argument you can optionally use to set attributes.
 *   - options.define("distort:viewport", "WxH+X+Y")
 *     - Specify the size and offset of the generated viewport image of the distorted image space. W and
 *       H are the width and height, and X and Y are the offset.
 *   - options.define("distort:scale", N)
 *     - N is an integer factor. Scale the output image (viewport or otherwise) by that factor without
 *       changing the viewed contents of the distorted image. This can be used either for
 *       'super-sampling' the image for a higher quality result, or for panning and zooming around
 *       the image (with appropriate viewport changes, or post-distort cropping and resizing).
 *   - options.verbose(true)
 *     - Attempt to output the internal coefficients, and the -fx equivalent to the distortion, for
         expert study, and debugging purposes. This many not be available for all distorts.
 *   @param type [Magick::DistortMethod] a DistortMethod value
 *   @param points [Array<Numeric>] an Array of Numeric values. The size of the array depends on the distortion type.
 *   @param bestfit [Boolean] If bestfit is enabled, and the distortion allows it, the destination
 *     image is adjusted to ensure the whole source image will just fit within the final destination
 *     image, which will be sized and offset accordingly.  Also in many cases the virtual offset of
 *     the source image will be taken into account in the mapping.
 *   @yield [opt_args]
 *   @yieldparam opt_args [Magick::OptionalMethodArguments]
 *
 * @return [Magick::Image] a new image
 * @example
 *   img.distort(Magick::ScaleRotateTranslateDistortion, [0]) do |options|
 *     options.define "distort:viewport", "44x44+15+0"
 *     options.define "distort:scale", 2
 *   end
 */
VALUE
Image_distort(int argc, VALUE *argv, VALUE self)
{
    Image *image, *new_image;
    VALUE pts;
    unsigned long n, npoints;
    DistortMethod distortion_method;
    double *points;
    MagickBooleanType bestfit = MagickFalse;
    ExceptionInfo *exception;

    image = rm_check_destroyed(self);
    rm_get_optional_arguments(self);

    switch (argc)
    {
        case 3:
            bestfit = (MagickBooleanType)RTEST(argv[2]);
        case 2:
            // Ensure pts is an array
            pts = rb_Array(argv[1]);
            VALUE_TO_ENUM(argv[0], distortion_method, DistortMethod);
            break;
        default:
            rb_raise(rb_eArgError, "wrong number of arguments (expected 2 or 3, got %d)", argc);
            break;
    }

    npoints = RARRAY_LEN(pts);
    points = ALLOC_N(double, npoints);

    for (n = 0; n < npoints; n++)
    {
        VALUE element = rb_ary_entry(pts, n);
        if (rm_check_num2dbl(element))
        {
            points[n] = NUM2DBL(element);
        }
        else
        {
            xfree(points);
            rb_raise(rb_eTypeError, "type mismatch: %s given", rb_class2name(CLASS_OF(element)));
        }
    }

    exception = AcquireExceptionInfo();
    GVL_STRUCT_TYPE(DistortImage) args = { image, distortion_method, npoints, points, bestfit, exception };
    new_image = (Image *)CALL_FUNC_WITHOUT_GVL(GVL_FUNC(DistortImage), &args);
    xfree(points);
    rm_check_exception(exception, new_image, DestroyOnError);
    DestroyExceptionInfo(exception);

    RB_GC_GUARD(pts);

    return rm_image_new(new_image);
}


/**
 * Compares one or more image channels of an image to a reconstructed image and returns the
 * specified distortion metric.
 *
 * @overload distortion_channel(reconstructed_image, metric, channel = Magick::AllChannels)
 *   @param reconstructed_image [Magick::Image, Magick::ImageList] Either an imagelist or an
 *     image. If an imagelist, uses the current image.
 *   @param metric [Magick::MetricType] The desired distortion metric.
 *   @param channel [Magick::ChannelType] a ChannelType arguments.
 *
 * @overload distortion_channel(reconstructed_image, metric, *channels)
 *   @param reconstructed_image [Magick::Image, Magick::ImageList] Either an imagelist or an
 *     image. If an imagelist, uses the current image.
 *   @param metric [Magick::MetricType] The desired distortion metric.
 *   @param *channels [Magick::ChannelType] one or more ChannelType arguments.
 *
 * @return [Float] the image channel distortion
 */
VALUE
Image_distortion_channel(int argc, VALUE *argv, VALUE self)
{
    Image *image, *reconstruct;
    ChannelType channels;
    ExceptionInfo *exception;
    MetricType metric;
    VALUE rec;
    double distortion;
#if defined(IMAGEMAGICK_7)
    Image *difference_image;
#endif

    image = rm_check_destroyed(self);
    channels = extract_channels(&argc, argv);
    if (argc > 2)
    {
        raise_ChannelType_error(argv[argc-1]);
    }
    if (argc < 2)
    {
        rb_raise(rb_eArgError, "wrong number of arguments (%d for 2 or more)", argc);
    }

    rec = rm_cur_image(argv[0]);
    reconstruct = rm_check_destroyed(rec);
    VALUE_TO_ENUM(argv[1], metric, MetricType);
    exception = AcquireExceptionInfo();
#if defined(IMAGEMAGICK_7)
    BEGIN_CHANNEL_MASK(image, channels);
    GVL_STRUCT_TYPE(CompareImages) args = { image, reconstruct, metric, &distortion, exception };
    difference_image = (Image *)CALL_FUNC_WITHOUT_GVL(GVL_FUNC(CompareImages), &args);
    END_CHANNEL_MASK(image);
    DestroyImage(difference_image);
#else
    GVL_STRUCT_TYPE(GetImageChannelDistortion) args = { image, reconstruct, channels, metric, &distortion, exception };
    CALL_FUNC_WITHOUT_GVL(GVL_FUNC(GetImageChannelDistortion), &args);
#endif

    CHECK_EXCEPTION();

    DestroyExceptionInfo(exception);

    RB_GC_GUARD(rec);

    return rb_float_new(distortion);
}


/**
 * Implement marshalling.
 *
 * @param depth [Object] unused
 * @return [String] a string representing the dumped image
 */
VALUE
Image__dump(VALUE self, VALUE depth ATTRIBUTE_UNUSED)
{
    Image *image;
    ImageInfo *info;
    void *blob;
    size_t length;
    DumpedImage mi;
    VALUE str;
    ExceptionInfo *exception;

    image = rm_check_destroyed(self);

    info = CloneImageInfo(NULL);
    if (!info)
    {
        rb_raise(rb_eNoMemError, "not enough memory to continue");
    }
    strlcpy(info->magick, image->magick, sizeof(info->magick));

    exception = AcquireExceptionInfo();
    GVL_STRUCT_TYPE(ImageToBlob) args = { info, image, &length, exception };
    blob = CALL_FUNC_WITHOUT_GVL(GVL_FUNC(ImageToBlob), &args);

    // Free ImageInfo first - error handling may raise an exception
    DestroyImageInfo(info);

    CHECK_EXCEPTION();

    DestroyExceptionInfo(exception);

    if (!blob)
    {
        rb_raise(rb_eNoMemError, "not enough memory to continue");
    }

    // Create a header for the blob: ID and version
    // numbers, followed by the length of the magick
    // string stored as a byte, followed by the
    // magick string itself.
    mi.id = DUMPED_IMAGE_ID;
    mi.mj = DUMPED_IMAGE_MAJOR_VERS;
    mi.mi = DUMPED_IMAGE_MINOR_VERS;
    strlcpy(mi.magick, image->magick, sizeof(mi.magick));
    mi.len = (unsigned char) min((size_t)UCHAR_MAX, rm_strnlen_s(mi.magick, sizeof(mi.magick)));

    // Concatenate the blob onto the header & return the result
    str = rb_str_new((char *)&mi, (long)(mi.len+offsetof(DumpedImage, magick)));
    str = rb_str_buf_cat(str, (char *)blob, (long)length);
    magick_free((void*)blob);

    RB_GC_GUARD(str);

    return str;
}


/**
 * Duplicates a image.
 *
 * @return [Magick::Image] a new image
 */
VALUE
Image_dup(VALUE self)
{
    VALUE dup;

    rm_check_destroyed(self);
    dup = TypedData_Wrap_Struct(CLASS_OF(self), &rm_image_data_type, NULL);
    RB_GC_GUARD(dup);

    return rb_funcall(dup, rm_ID_initialize_copy, 1, self);
}


/**
 * Calls block once for each profile in the image, passing the profile name and value as parameters.
 *
 * @yield [name, val]
 * @yieldparam name [String] the profile name
 * @yieldparam val [String] the profile value
 * @return [Object] the last value returned by the block
 */
VALUE
Image_each_profile(VALUE self)
{
    Image *image;
    VALUE ary;
    VALUE val = Qnil;
    char *name;
    const StringInfo *profile;

    image = rm_check_destroyed(self);
    ResetImageProfileIterator(image);

    ary = rb_ary_new2(2);

    name = GetNextImageProfile(image);
    while (name)
    {
        rb_ary_store(ary, 0, rb_str_new2(name));

        profile = GetImageProfile(image, name);
        if (!profile)
        {
            rb_ary_store(ary, 1, Qnil);
        }
        else
        {
            rb_ary_store(ary, 1, rb_str_new((char *)profile->datum, (long)profile->length));
        }
        val = rb_yield(ary);
        name = GetNextImageProfile(image);
    }

    RB_GC_GUARD(ary);
    RB_GC_GUARD(val);

    return val;
}


/**
 * Find edges in an image. "radius" defines the radius of the convolution filter.
 *
 * @overload edge(radius = 0.0)
 *   @param radius [Numeric] The radius of the convolution filter.
 *   @return [Magick::Image] a new image
 */
VALUE
Image_edge(int argc, VALUE *argv, VALUE self)
{
    Image *image, *new_image;
    double radius = 0.0;
    ExceptionInfo *exception;

    image = rm_check_destroyed(self);
    switch (argc)
    {
        case 1:
            radius = NUM2DBL(argv[0]);
        case 0:
            break;
        default:
            rb_raise(rb_eArgError, "wrong number of arguments (%d for 0 or 1)", argc);
            break;
    }

    exception = AcquireExceptionInfo();

    GVL_STRUCT_TYPE(EdgeImage) args = { image, radius, exception };
    new_image = (Image *)CALL_FUNC_WITHOUT_GVL(GVL_FUNC(EdgeImage), &args);
    rm_check_exception(exception, new_image, DestroyOnError);
    DestroyExceptionInfo(exception);

    return rm_image_new(new_image);
}


// aliases for common use of structure types; BlurImage, CharcoalImage, EmbossImage, GaussianBlurImage, SharpenImage
typedef GVL_STRUCT_TYPE(BlurImage) GVL_STRUCT_TYPE(effect_image);

/**
 * Call one of the effects methods.
 *
 * No Ruby usage (internal function)
 *
 * @param self this object
 * @param argc number of input arguments
 * @param argv array of input arguments
 * @param effector the effector to call
 * @return a new image
 */
static VALUE
effect_image(VALUE self, int argc, VALUE *argv, gvl_function_t fp)
{
    Image *image, *new_image;
    ExceptionInfo *exception;
    double radius = 0.0, sigma = 1.0;

    image = rm_check_destroyed(self);

    switch (argc)
    {
        case 2:
            sigma = NUM2DBL(argv[1]);
        case 1:
            radius = NUM2DBL(argv[0]);
        case 0:
            break;
        default:
            rb_raise(rb_eArgError, "wrong number of arguments (%d for 0 to 2)", argc);
            break;
    }

    if (sigma == 0.0)
    {
        rb_raise(rb_eArgError, "sigma must be != 0.0");
    }

    exception = AcquireExceptionInfo();
    GVL_STRUCT_TYPE(effect_image) args = { image, radius, sigma, exception };
    new_image = (Image *)CALL_FUNC_WITHOUT_GVL(fp, &args);
    rm_check_exception(exception, new_image, DestroyOnError);
    DestroyExceptionInfo(exception);

    return rm_image_new(new_image);
}


/**
 * Adds a 3-dimensional effect.
 *
 * @overload emboss(radius = 0.0, sigma = 1.0)
 *   @param radius [Numeric] The radius of the Gaussian operator.
 *   @param sigma [Numeric] The sigma (standard deviation) of the Gaussian operator.
 *   @return [Magick::Image] a new image
 */
VALUE
Image_emboss(int argc, VALUE *argv, VALUE self)
{
    return effect_image(self, argc, argv, GVL_FUNC(EmbossImage));
}


/**
 * Encipher an image.
 *
 * @param passphrase [String] the passphrase with which to encipher
 * @return [Magick::Image] a new image
 * @example
 *   enciphered_img = img.encipher("magic word")
 */
VALUE
Image_encipher(VALUE self, VALUE passphrase)
{
    Image *image, *new_image;
    char *pf;
    ExceptionInfo *exception;
    MagickBooleanType okay;

    image = rm_check_destroyed(self);
    pf = StringValueCStr(passphrase);      // ensure passphrase is a string
    exception = AcquireExceptionInfo();

    new_image = rm_clone_image(image);

    GVL_STRUCT_TYPE(EncipherImage) args = { new_image, pf, exception };
    void *ret = CALL_FUNC_WITHOUT_GVL(GVL_FUNC(EncipherImage), &args);
    okay = static_cast<MagickBooleanType>(reinterpret_cast<intptr_t &>(ret));
    rm_check_exception(exception, new_image, DestroyOnError);
    if (!okay)
    {
        DestroyImage(new_image);
        rb_raise(rb_eRuntimeError, "EncipherImage failed for unknown reason.");
    }

    DestroyExceptionInfo(exception);

    return rm_image_new(new_image);
}



/**
 * Return endian option for images that support it.
 *
 * @return [Magick::EndianType] the endian option
 */
VALUE
Image_endian(VALUE self)
{
    Image *image = rm_check_destroyed(self);
    return EndianType_find(image->endian);
}


/**
 * Set endian option for images that support it.
 *
 * @param type [Magick::EndianType] the endian type
 * @return [Magick::EndianType] the given type
 */
VALUE
Image_endian_eq(VALUE self, VALUE type)
{
    Image *image = rm_check_frozen(self);
    VALUE_TO_ENUM(type, image->endian, EndianType);
    return type;
}

/**
 * Apply a digital filter that improves the quality of a noisy image.
 *
 * @return [Magick::Image] a new image
 */
VALUE
Image_enhance(VALUE self)
{
    Image *image, *new_image;
    ExceptionInfo *exception;

    image = rm_check_destroyed(self);
    exception = AcquireExceptionInfo();

    GVL_STRUCT_TYPE(EnhanceImage) args = { image, exception };
    new_image = (Image *)CALL_FUNC_WITHOUT_GVL(GVL_FUNC(EnhanceImage), &args);
    rm_check_exception(exception, new_image, DestroyOnError);
    DestroyExceptionInfo(exception);

    return rm_image_new(new_image);
}


/**
 * Apply a histogram equalization to the image.
 *
 * @return [Magick::Image] a new image
 */
VALUE
Image_equalize(VALUE self)
{
    Image *image, *new_image;
#if defined(IMAGEMAGICK_7)
    ExceptionInfo *exception;
#endif

    image = rm_check_destroyed(self);
    new_image = rm_clone_image(image);

#if defined(IMAGEMAGICK_7)
    exception = AcquireExceptionInfo();
    GVL_STRUCT_TYPE(EqualizeImage) args = { new_image, exception };
    CALL_FUNC_WITHOUT_GVL(GVL_FUNC(EqualizeImage), &args);
    rm_check_exception(exception, new_image, DestroyOnError);
    DestroyExceptionInfo(exception);
#else
    GVL_STRUCT_TYPE(EqualizeImage) args = { new_image };
    CALL_FUNC_WITHOUT_GVL(GVL_FUNC(EqualizeImage), &args);
    rm_check_image_exception(new_image, DestroyOnError);
#endif

    return rm_image_new(new_image);
}


/**
 * Applies a histogram equalization to the image. Only the specified channels are equalized.
 *
 * @overload equalize_channel(channel = Magick::AllChannels)
 *   @param channel [Magick::ChannelType] a ChannelType arguments.
 *
 * @overload equalize_channel(*channels)
 *   @param *channels [Magick::ChannelType] one or more ChannelType arguments.
 *
 * @return [Magick::Image] a new image
 */
VALUE
Image_equalize_channel(int argc, VALUE *argv, VALUE self)
{
    Image *image, *new_image;
#if defined(IMAGEMAGICK_7)
    ExceptionInfo *exception;
#endif
    ChannelType channels;

    image = rm_check_destroyed(self);
    channels = extract_channels(&argc, argv);
    if (argc > 0)
    {
        raise_ChannelType_error(argv[argc-1]);
    }

    new_image = rm_clone_image(image);

#if defined(IMAGEMAGICK_7)
    exception = AcquireExceptionInfo();
    BEGIN_CHANNEL_MASK(new_image, channels);
    GVL_STRUCT_TYPE(EqualizeImage) args = { new_image, exception };
    CALL_FUNC_WITHOUT_GVL(GVL_FUNC(EqualizeImage), &args);
    END_CHANNEL_MASK(new_image);
    rm_check_exception(exception, new_image, DestroyOnError);
    DestroyExceptionInfo(exception);
#else
    GVL_STRUCT_TYPE(EqualizeImageChannel) args = { new_image, channels };
    CALL_FUNC_WITHOUT_GVL(GVL_FUNC(EqualizeImageChannel), &args);

    rm_check_image_exception(new_image, DestroyOnError);
#endif

    return rm_image_new(new_image);
}


/**
 * Reset the image to the background color.
 *
 * @return [Magick::Image] self
 */
VALUE
Image_erase_bang(VALUE self)
{
    Image *image;
#if defined(IMAGEMAGICK_7)
    ExceptionInfo *exception;
#endif

    image = rm_check_frozen(self);

#if defined(IMAGEMAGICK_7)
    exception = AcquireExceptionInfo();
    GVL_STRUCT_TYPE(SetImageBackgroundColor) args = { image, exception };
    CALL_FUNC_WITHOUT_GVL(GVL_FUNC(SetImageBackgroundColor), &args);
    CHECK_EXCEPTION();
    DestroyExceptionInfo(exception);
#else
    GVL_STRUCT_TYPE(SetImageBackgroundColor) args = { image };
    CALL_FUNC_WITHOUT_GVL(GVL_FUNC(SetImageBackgroundColor), &args);
    rm_check_image_exception(image, RetainOnError);
#endif

    return self;
}


/**
 * Lightweight crop.
 *
 * No Ruby usage (internal function)
 *
 * Notes:
 *   - christy says "does not respect the virtual page offset (-page) and does
 *     not update the page offset and its more efficient than cropping."
 *
 * @param bang whether the bang (!) version of the method was called
 * @param self this object
 * @param x the x position for the start of the rectangle
 * @param y the y position for the start of the rectangle
 * @param width the width of the rectancle
 * @param height the height of the rectangle
 * @return self if bang, otherwise a new image
 * @see Image_excerpt
 * @see Image_excerpt_bang
 * @see Image_crop
 * @see Image_crop_bang
 */
static VALUE
excerpt(int bang, VALUE self, VALUE x, VALUE y, VALUE width, VALUE height)
{
    Image *image, *new_image;
    RectangleInfo rect;
    ExceptionInfo *exception;

    memset(&rect, '\0', sizeof(rect));
    rect.x = NUM2LONG(x);
    rect.y = NUM2LONG(y);
    rect.width = NUM2ULONG(width);
    rect.height = NUM2ULONG(height);

    TypedData_Get_Struct(self, Image, &rm_image_data_type, image);

    exception = AcquireExceptionInfo();
    GVL_STRUCT_TYPE(ExcerptImage) args = { image, &rect, exception };
    new_image = (Image *)CALL_FUNC_WITHOUT_GVL(GVL_FUNC(ExcerptImage), &args);
    rm_check_exception(exception, new_image, DestroyOnError);
    DestroyExceptionInfo(exception);

    if (bang)
    {
        rm_ensure_result(new_image);
        UPDATE_DATA_PTR(self, new_image);
        rm_image_destroy(image);
        return self;
    }

    return rm_image_new(new_image);
}


/**
 * This method is very similar to crop.
 * It extracts the rectangle specified by its arguments from the image and returns it as a new
 * image. However, excerpt does not respect the virtual page offset and does not update the page
 * offset and is more efficient than cropping.
 *
 * @param x [Numeric] the x position for the start of the rectangle
 * @param y [Numeric] the y position for the start of the rectangle
 * @param width [Numeric] the width of the rectancle
 * @param height [Numeric] the height of the rectangle
 * @return [Magick::Image] a new image
 * @see Image#excerpt!
 * @see Image#crop
 * @see Image#crop!
 */
VALUE
Image_excerpt(VALUE self, VALUE x, VALUE y, VALUE width, VALUE height)
{
    rm_check_destroyed(self);
    return excerpt(False, self, x, y, width, height);
}


/**
 * In-place form of {Magick::Image#excerpt}.
 *
 * This method is very similar to crop.
 * It extracts the rectangle specified by its arguments from the image and returns it as a new
 * image.  However, excerpt does not respect the virtual page offset and does not update the page
 * offset and is more efficient than cropping.
 *
 * @param x [Numeric] the x position for the start of the rectangle
 * @param y [Numeric] the y position for the start of the rectangle
 * @param width [Numeric] the width of the rectancle
 * @param height [Numeric] the height of the rectangle
 * @return [Magick::Image] self
 * @see Image#excerpt
 * @see Image#crop
 * @see Image#crop!
 */
VALUE
Image_excerpt_bang(VALUE self, VALUE x, VALUE y, VALUE width, VALUE height)
{
    rm_check_frozen(self);
    return excerpt(True, self, x, y, width, height);
}


/**
 * Extracts the pixel data from the specified rectangle and returns it as an array of Integer
 * values. The array returned by {Magick::Image#export_pixels} is suitable for use as an argument
 * to {Magick::Image#import_pixels}.
 *
 * @overload export_pixels(x = 0, y = 0, cols = self.columns, rows = self.rows, map = "RGB")
 *   @param x [Numeric] The offset of the rectangle from the upper-left corner of the image.
 *   @param y [Numeric] The offset of the rectangle from the upper-left corner of the image.
 *   @param cols [Numeric] The width of the rectangle.
 *   @param rows [Numeric] The height of the rectangle.

 *   @param map [String] A string that describes which pixel channel data is desired and the order
 *     in which it should be stored. It can be any combination or order of R = red, G = green, B =
 *     blue, A = alpha, C = cyan, Y = yellow, M = magenta, K = black, I = intensity (for grayscale),
 *     or P = pad.
 *   @return [Array<Numeric>] array of pixels
 */
VALUE
Image_export_pixels(int argc, VALUE *argv, VALUE self)
{
    Image *image;
    long x_off = 0L, y_off = 0L;
    unsigned long cols, rows;
    long n, npixels;
    MagickBooleanType okay;
    const char *map = "RGB";
    Quantum *pixels;
    VALUE ary;
    ExceptionInfo *exception;


    image = rm_check_destroyed(self);
    cols = image->columns;
    rows = image->rows;

    switch (argc)
    {
        case 5:
            map   = StringValueCStr(argv[4]);
        case 4:
            rows  = NUM2ULONG(argv[3]);
        case 3:
            cols  = NUM2ULONG(argv[2]);
        case 2:
            y_off = NUM2LONG(argv[1]);
        case 1:
            x_off = NUM2LONG(argv[0]);
        case 0:
            break;
        default:
            rb_raise(rb_eArgError, "wrong number of arguments (%d for 0 to 5)", argc);
            break;
    }

    if (   x_off < 0 || (unsigned long)x_off > image->columns
           || y_off < 0 || (unsigned long)y_off > image->rows
           || cols == 0 || rows == 0)
    {
        rb_raise(rb_eArgError, "invalid extract geometry");
    }


    npixels = (long)(cols * rows * strlen(map));
    pixels = ALLOC_N(Quantum, npixels);
    if (!pixels)    // app recovered from exception
    {
        return rb_ary_new2(0L);
    }

    exception = AcquireExceptionInfo();

    GVL_STRUCT_TYPE(ExportImagePixels) args = { image, x_off, y_off, cols, rows, map, QuantumPixel, (void *)pixels, exception };
    void *ret = CALL_FUNC_WITHOUT_GVL(GVL_FUNC(ExportImagePixels), &args);
    okay = static_cast<MagickBooleanType>(reinterpret_cast<intptr_t &>(ret));
    if (!okay)
    {
        xfree((void *)pixels);
        CHECK_EXCEPTION();

        // Should never get here...
        rm_magick_error("ExportImagePixels failed with no explanation.");
    }

    DestroyExceptionInfo(exception);

    ary = rb_ary_new2(npixels);
    for (n = 0; n < npixels; n++)
    {
        rb_ary_push(ary, QUANTUM2NUM(pixels[n]));
    }

    xfree((void *)pixels);

    RB_GC_GUARD(ary);

    return ary;
}


/**
 * If width or height is greater than the target image's width or height, extends the width and
 * height of the target image to the specified values.  The new pixels are set to the background
 * color. If width or height is less than the target image's width or height, crops the target
 * image.
 *
 * @overload extent(width, height, x = 0, y = 0)
 *   @param width [Numeric] The width of the new image
 *   @param height [Numeric] The height of the new image
 *   @param x [Numeric] The upper-left corner of the new image is positioned
 *   @param y [Numeric] The upper-left corner of the new image is positioned
 *   @return [Magick::Image] a new image
 */
VALUE
Image_extent(int argc, VALUE *argv, VALUE self)
{
    Image *image, *new_image;
    RectangleInfo geometry;
    long height, width;
    ExceptionInfo *exception;

    rm_check_destroyed(self);

    if (argc < 2 || argc > 4)
    {
        rb_raise(rb_eArgError, "wrong number of arguments (expected 2 to 4, got %d)", argc);
    }

    geometry.y = geometry.x = 0L;
    switch (argc)
    {
        case 4:
            geometry.y = NUM2LONG(argv[3]);
        case 3:
            geometry.x = NUM2LONG(argv[2]);
        default:
            geometry.height = height = NUM2LONG(argv[1]);
            geometry.width = width = NUM2LONG(argv[0]);
            break;
    }

    // Use the signed versions of these two values to test for < 0
    if (height <= 0L || width <= 0L)
    {
        if (geometry.x == 0 && geometry.y == 0)
        {
            rb_raise(rb_eArgError, "invalid extent geometry %ldx%ld", width, height);
        }
        else
        {
            rb_raise(rb_eArgError, "invalid extent geometry %ldx%ld+%" RMIdSIZE "+%" RMIdSIZE "",
                     width, height, geometry.x, geometry.y);
        }
    }


    TypedData_Get_Struct(self, Image, &rm_image_data_type, image);
    exception = AcquireExceptionInfo();

    GVL_STRUCT_TYPE(ExtentImage) args = { image, &geometry, exception };
    new_image = (Image *)CALL_FUNC_WITHOUT_GVL(GVL_FUNC(ExtentImage), &args);
    rm_check_exception(exception, new_image, DestroyOnError);
    DestroyExceptionInfo(exception);

    return rm_image_new(new_image);
}


/**
 * Extracts the pixel data from the specified rectangle and returns it as a string.
 *
 * @overload export_pixels_to_str(x = 0, y = 0, cols = self.columns, rows = self.rows, map = "RGB", type = Magick::CharPixel)
 *   @param x [Numeric] The offset of the rectangle from the upper-left corner of the image.
 *   @param y [Numeric] The offset of the rectangle from the upper-left corner of the image.
 *   @param cols [Numeric] The width of the rectangle.
 *   @param rows [Numeric] The height of the rectangle.
 *   @param map [String] A string that describes which pixel channel data is desired and the order
 *     in which it should be stored. It can be any combination or order of R = red, G = green, B =
 *     blue, A = alpha, C = cyan, Y = yellow, M = magenta, K = black, I = intensity (for grayscale),
 *     or P = pad.
 *   @param type [Magick::StorageType] A StorageType value that specifies the C datatype to which
 *     the pixel data will be converted.
 *   @return [String] the pixel data
 */
VALUE
Image_export_pixels_to_str(int argc, VALUE *argv, VALUE self)
{
    Image *image;
    long x_off = 0L, y_off = 0L;
    unsigned long cols, rows;
    unsigned long npixels;
    size_t sz;
    MagickBooleanType okay;
    const char *map = "RGB";
    StorageType type = CharPixel;
    VALUE string;
    ExceptionInfo *exception;

    image = rm_check_destroyed(self);
    cols = image->columns;
    rows = image->rows;

    switch (argc)
    {
        case 6:
            VALUE_TO_ENUM(argv[5], type, StorageType);
        case 5:
            map   = StringValueCStr(argv[4]);
        case 4:
            rows  = NUM2ULONG(argv[3]);
        case 3:
            cols  = NUM2ULONG(argv[2]);
        case 2:
            y_off = NUM2LONG(argv[1]);
        case 1:
            x_off = NUM2LONG(argv[0]);
        case 0:
            break;
        default:
            rb_raise(rb_eArgError, "wrong number of arguments (%d for 0 to 6)", argc);
            break;
    }

    if (   x_off < 0 || (unsigned long)x_off > image->columns
           || y_off < 0 || (unsigned long)y_off > image->rows
           || cols == 0 || rows == 0)
    {
        rb_raise(rb_eArgError, "invalid extract geometry");
    }


    npixels = cols * rows * strlen(map);
    switch (type)
    {
        case CharPixel:
            sz = sizeof(unsigned char);
            break;
        case ShortPixel:
            sz = sizeof(unsigned short);
            break;
        case DoublePixel:
            sz = sizeof(double);
            break;
        case FloatPixel:
            sz = sizeof(float);
            break;
        case LongPixel:
            sz = sizeof(unsigned long);
            break;
        case QuantumPixel:
            sz = sizeof(Quantum);
            break;
        case UndefinedPixel:
        default:
            rb_raise(rb_eArgError, "undefined storage type");
            break;
    }

    // Allocate a string long enough to hold the exported pixel data.
    // Get a pointer to the buffer.
    string = rb_str_new2("");
    rb_str_resize(string, (long)(sz * npixels));

    exception = AcquireExceptionInfo();

    GVL_STRUCT_TYPE(ExportImagePixels) args = { image, x_off, y_off, cols, rows, map, type, (void *)RSTRING_PTR(string), exception };
    void *ret = CALL_FUNC_WITHOUT_GVL(GVL_FUNC(ExportImagePixels), &args);
    okay = static_cast<MagickBooleanType>(reinterpret_cast<intptr_t &>(ret));
    if (!okay)
    {
        // Let GC have the string buffer.
        rb_str_resize(string, 0);
        CHECK_EXCEPTION();

        // Should never get here...
        rm_magick_error("ExportImagePixels failed with no explanation.");
    }

    DestroyExceptionInfo(exception);

    RB_GC_GUARD(string);

    return string;
}


/**
 * The extract_info attribute reader.
 *
 * @return [Magick::Rectangle] the Rectangle object
 */
VALUE
Image_extract_info(VALUE self)
{
    Image *image = rm_check_destroyed(self);
    return Import_RectangleInfo(&image->extract_info);
}


/**
 * Set the extract_info attribute.
 *
 * @param rect [Magick::Rectangle] the Rectangle object
 * @return [Magick::Rectangle] the given value
 */
VALUE
Image_extract_info_eq(VALUE self, VALUE rect)
{
    Image *image = rm_check_frozen(self);
    Export_RectangleInfo(&image->extract_info, rect);
    return rect;
}


/**
 * Get image filename.
 *
 * @return [String] the filename
 */
VALUE
Image_filename(VALUE self)
{
    IMPLEMENT_TYPED_ATTR_READER(Image, filename, str, &rm_image_data_type);
}


/**
 * Return the image file size.
 *
 * @return [Integer] the file size
 */
VALUE Image_filesize(VALUE self)
{
    Image *image = rm_check_destroyed(self);
    return INT2FIX(GetBlobSize(image));
}


/**
 * Get filter type.
 *
 * @return [Magick::FilterType] the filter
 */
VALUE
Image_filter(VALUE self)
{
    Image *image = rm_check_destroyed(self);
    return FilterType_find(image->filter);
}


/**
 * Set filter type.
 *
 * @param filter [Magick::FilterType] the filter
 * @return [Magick::FilterType] the given filter
 */
VALUE
Image_filter_eq(VALUE self, VALUE filter)
{
    Image *image = rm_check_frozen(self);
    VALUE_TO_ENUM(filter, image->filter, FilterType);
    return filter;
}


/**
 * This interesting method searches for a rectangle in the image that is similar to the target.
 * For the rectangle to be similar each pixel in the rectangle must match the corresponding pixel in
 * the target image within the range specified by the fuzz attributes of the image and the target
 * image.
 *
 * @overload find_similar_region(target, x = 0, y = 0)
 *   @param target [Magick::Image, Magick::ImageList] An image that forms the target of the
 *     search. This image can be any size. Either an imagelist or an image. If an imagelist, uses
 *     the current image.
 *   @param x [Numeric] The starting x-offsets for the search.
 *   @param y [Numeric] The starting y-offsets for the search.
 *   @return [Array<Numeric>, nil] If the search succeeds, the return value is an array with 2 elements.
 *     These elements are the x- and y-offsets of the matching rectangle.
 *     If the search fails the return value is nil.
 */
VALUE
Image_find_similar_region(int argc, VALUE *argv, VALUE self)
{
    Image *image, *target;
    VALUE region, targ;
    ssize_t x = 0L, y = 0L;
    ExceptionInfo *exception;
    MagickBooleanType okay;

    image = rm_check_destroyed(self);

    switch (argc)
    {
        case 3:
            y = NUM2LONG(argv[2]);
        case 2:
            x = NUM2LONG(argv[1]);
        case 1:
            targ = rm_cur_image(argv[0]);
            target = rm_check_destroyed(targ);
            break;
        default:
            rb_raise(rb_eArgError, "wrong number of arguments (%d for 1 to 3)", argc);
            break;
    }

    exception = AcquireExceptionInfo();
#if defined(IMAGEMAGICK_7)
    GVL_STRUCT_TYPE(IsEquivalentImage) args = { image, target, &x, &y, exception };
    void *ret = CALL_FUNC_WITHOUT_GVL(GVL_FUNC(IsEquivalentImage), &args);
#else
    GVL_STRUCT_TYPE(IsImageSimilar) args = { image, target, &x, &y, exception };
    void *ret = CALL_FUNC_WITHOUT_GVL(GVL_FUNC(IsImageSimilar), &args);
#endif
    okay = static_cast<MagickBooleanType>(reinterpret_cast<intptr_t &>(ret));
    CHECK_EXCEPTION();
    DestroyExceptionInfo(exception);

    if (!okay)
    {
        return Qnil;
    }

    region = rb_ary_new2(2);
    rb_ary_store(region, 0L, LONG2NUM(x));
    rb_ary_store(region, 1L, LONG2NUM(y));

    RB_GC_GUARD(region);
    RB_GC_GUARD(targ);

    return region;
}


// aliases for common use of structure types; FlopImage, FlipImage
typedef GVL_STRUCT_TYPE(FlipImage) GVL_STRUCT_TYPE(flipflop);

/**
 * Call a flipflopper (a function that either flips or flops the image).
 *
 * No Ruby usage (internal function)
 *
 * @param bang whether the bang (!) version of the method was called
 * @param self this object
 * @param flipflopper the flip/flop method to call
 * @return self if bang, otherwise a new image
 * @see Image_flip
 * @see Image_flip_bang
 * @see Image_flop
 * @see Image_flop_bang
 */
static VALUE
flipflop(int bang, VALUE self, gvl_function_t fp)
{
    Image *image, *new_image;
    ExceptionInfo *exception;

    TypedData_Get_Struct(self, Image, &rm_image_data_type, image);
    exception = AcquireExceptionInfo();

    GVL_STRUCT_TYPE(flipflop) args = { image, exception };
    new_image = (Image *)CALL_FUNC_WITHOUT_GVL(fp, &args);
    rm_check_exception(exception, new_image, DestroyOnError);
    DestroyExceptionInfo(exception);

    if (bang)
    {
        rm_ensure_result(new_image);
        UPDATE_DATA_PTR(self, new_image);
        rm_image_destroy(image);
        return self;
    }

    return rm_image_new(new_image);
}


/**
 * Create a vertical mirror image by reflecting the pixels around the central x-axis.
 *
 * @return [Magick::Image] a new image
 * @see Image#flip!
 * @see Image#flop
 * @see Image#flop!
 */
VALUE
Image_flip(VALUE self)
{
    rm_check_destroyed(self);
    return flipflop(False, self, GVL_FUNC(FlipImage));
}


/**
 * Create a vertical mirror image by reflecting the pixels around the central x-axis.
 * In-place form of {Image#flip}.
 *
 * @return [Magick::Image] self
 * @see Image#flip
 * @see Image#flop
 * @see Image#flop!
 */
VALUE
Image_flip_bang(VALUE self)
{
    rm_check_frozen(self);
    return flipflop(True, self, GVL_FUNC(FlipImage));
}


/**
 * Create a horizonal mirror image by reflecting the pixels around the central y-axis.
 *
 * @return [Magick::Image] a new image
 * @see Image#flop!
 * @see Image#flip
 * @see Image#flip!
 */
VALUE
Image_flop(VALUE self)
{
    rm_check_destroyed(self);
    return flipflop(False, self, GVL_FUNC(FlopImage));
}


/**
 * Create a horizonal mirror image by reflecting the pixels around the central y-axis.
 * In-place form of {Image#flop}.
 *
 * @return [Magick::Image] self
 * @see Image#flop
 * @see Image#flip
 * @see Image#flip!
 */
VALUE
Image_flop_bang(VALUE self)
{
    rm_check_frozen(self);
    return flipflop(True, self, GVL_FUNC(FlopImage));
}


/**
 * Return the image encoding format. For example, "GIF" or "PNG".
 *
 * @return [String, nil] the encoding format
 */
VALUE
Image_format(VALUE self)
{
    Image *image;
    const MagickInfo *magick_info;
    ExceptionInfo *exception;

    image = rm_check_destroyed(self);

    if (*image->magick)
    {
        // Deliberately ignore the exception info!
        exception = AcquireExceptionInfo();
        magick_info = GetMagickInfo(image->magick, exception);
        DestroyExceptionInfo(exception);
        return magick_info ? rb_str_new2(magick_info->name) : Qnil;
    }

    return Qnil;
}


/**
 * Set the image encoding format. For example, "GIF" or "PNG".
 *
 * @param magick [String] the encoding format
 * @return [String] the given value
 */
VALUE
Image_format_eq(VALUE self, VALUE magick)
{
    Image *image;
    const MagickInfo *m;
    char *mgk;
    ExceptionInfo *exception;

    image = rm_check_frozen(self);

    mgk = StringValueCStr(magick);

    exception = AcquireExceptionInfo();
    m = GetMagickInfo(mgk, exception);
    CHECK_EXCEPTION();

    DestroyExceptionInfo(exception);

    if (!m)
    {
        rb_raise(rb_eArgError, "unknown format: %s", mgk);
    }


    strlcpy(image->magick, m->name, sizeof(image->magick));
    return magick;
}


/**
 * Add a simulated three-dimensional border around the image.
 *
 * @overload frame(width = self.columns+25*2, height = self.rows+25*2, x = 25, y = 25, inner_bevel = 6, outer_bevel = 6, color = self.matte_color)
 *   @param width [Numeric] The width of the left and right sides.
 *   @param height [Numeric] The height of the top and bottom sides.
 *   @param x [Numeric] The offset of the image from the upper-left outside corner of the border.
 *   @param y [Numeric] The offset of the image from the upper-left outside corner of the border.
 *   @param inner_bevel [Numeric] The width of the inner shadows of the border.
 *   @param outer_bevel [Numeric] The width of the outer shadows of the border.
 *   @param color [Magick::Pixel, String] The border color.
 *   @return [Magick::Image] a new image.
 */
VALUE
Image_frame(int argc, VALUE *argv, VALUE self)
{
    Image *image, *new_image;
    ExceptionInfo *exception;
    FrameInfo frame_info;

    image = rm_check_destroyed(self);

    frame_info.width = image->columns + 50;
    frame_info.height = image->rows + 50;
    frame_info.x = 25;
    frame_info.y = 25;
    frame_info.inner_bevel = 6;
    frame_info.outer_bevel = 6;

    switch (argc)
    {
        case 7:
            Color_to_PixelColor(&image->matte_color, argv[6]);
        case 6:
            frame_info.outer_bevel = NUM2LONG(argv[5]);
        case 5:
            frame_info.inner_bevel = NUM2LONG(argv[4]);
        case 4:
            frame_info.y = NUM2LONG(argv[3]);
        case 3:
            frame_info.x = NUM2LONG(argv[2]);
        case 2:
            frame_info.height = image->rows + 2*NUM2LONG(argv[1]);
        case 1:
            frame_info.width = image->columns + 2*NUM2LONG(argv[0]);
        case 0:
            break;
        default:
            rb_raise(rb_eArgError, "wrong number of arguments (%d for 0 to 7)", argc);
            break;
    }

    exception = AcquireExceptionInfo();
#if defined(IMAGEMAGICK_7)
    GVL_STRUCT_TYPE(FrameImage) args = { image, &frame_info, image->compose, exception };
#else
    GVL_STRUCT_TYPE(FrameImage) args = { image, &frame_info, exception };
#endif
    new_image = (Image *)CALL_FUNC_WITHOUT_GVL(GVL_FUNC(FrameImage), &args);
    rm_check_exception(exception, new_image, DestroyOnError);
    DestroyExceptionInfo(exception);

    return rm_image_new(new_image);
}


/**
 * Convert direct to memory image formats from string data.
 *
 * @overload from_blob(blob)
 *   @param blob [String] the blob data
 *
 * @overload from_blob(blob)
 *   This yields {Magick::Image::Info} to block with its object's scope.
 *   @param blob [String] the blob data
 *   @yield [info]
 *   @yieldparam info [Magick::Image::Info]
 *
 * @return [Array<Magick::Image>] an array of new images
 * @see Image#to_blob
 */
VALUE
Image_from_blob(VALUE klass ATTRIBUTE_UNUSED, VALUE blob_arg)
{
    Image *images;
    Info *info;
    VALUE info_obj;
    ExceptionInfo *exception;
    void *blob;
    size_t length;

    blob = (void *) rm_str2cstr(blob_arg, &length);

    // Get a new Info object - run the parm block if supplied
    info_obj = rm_info_new();
    TypedData_Get_Struct(info_obj, Info, &rm_info_data_type, info);

    exception = AcquireExceptionInfo();
    GVL_STRUCT_TYPE(BlobToImage) args = { info,  blob, (size_t)length, exception };
    images = (Image *)CALL_FUNC_WITHOUT_GVL(GVL_FUNC(BlobToImage), &args);
    rm_check_exception(exception, images, DestroyOnError);

    DestroyExceptionInfo(exception);

    rm_ensure_result(images);
    rm_set_user_artifact(images, info);
    rm_sync_image_options(images, info);

    RB_GC_GUARD(info_obj);

    return array_from_images(images);
}


/**
 * Set the function on a channel.
 *
 * @overload function_channel(function, *args, channel = Magick::AllChannels)
 *   @param function [Magick::MagickFunction] the function
 *   @param *args [Numeric] One or more floating-point numbers.
 *     The number of parameters depends on the function. See the ImageMagick documentation for
 *     details.
 *   @param channel [Magick::ChannelType] a ChannelType arguments.
 *
 * @overload function_channel(function, *args, *channels)
 *   @param function [Magick::MagickFunction] the function
 *   @param *args [Numeric] One or more floating-point numbers.
 *     The number of parameters depends on the function. See the ImageMagick documentation for
 *     details.
 *   @param *channels [Magick::ChannelType] one or more ChannelType arguments.
 *
 * @return [Magick::Image] a new image
 * @see https://www.imagemagick.org/script/command-line-options.php#function
 */
VALUE
Image_function_channel(int argc, VALUE *argv, VALUE self)
{
    Image *image, *new_image;
    MagickFunction function;
    unsigned long n, nparms;
    double *parms;
    ChannelType channels;
    ExceptionInfo *exception;

    image = rm_check_destroyed(self);
    channels = extract_channels(&argc, argv);

    // The number of parameters depends on the function.
    if (argc == 0)
    {
        rb_raise(rb_eArgError, "no function specified");
    }

    VALUE_TO_ENUM(argv[0], function, MagickFunction);
    argc -= 1;
    argv += 1;

    switch (function)
    {
        case PolynomialFunction:
            if (argc == 0)
            {
                rb_raise(rb_eArgError, "PolynomialFunction requires at least one argument.");
            }
            break;
        case SinusoidFunction:
        case ArcsinFunction:
        case ArctanFunction:
           if (argc < 1 || argc > 4)
           {
               rb_raise(rb_eArgError, "wrong number of arguments (%d for 1 to 4)", argc);
           }
           break;
        default:
            rb_raise(rb_eArgError, "undefined function");
            break;
    }

    nparms = argc;
    parms = ALLOC_N(double, nparms);

    for (n = 0; n < nparms; n++)
    {
        VALUE element = argv[n];
        if (rm_check_num2dbl(element))
        {
            parms[n] = NUM2DBL(element);
        }
        else
        {
            xfree(parms);
            rb_raise(rb_eTypeError, "type mismatch: %s given", rb_class2name(CLASS_OF(element)));
        }
    }

    exception = AcquireExceptionInfo();
    new_image = rm_clone_image(image);
#if defined(IMAGEMAGICK_7)
    BEGIN_CHANNEL_MASK(new_image, channels);
    GVL_STRUCT_TYPE(FunctionImage) args = { new_image, function, nparms, parms, exception };
    CALL_FUNC_WITHOUT_GVL(GVL_FUNC(FunctionImage), &args);
    END_CHANNEL_MASK(new_image);
#else
    GVL_STRUCT_TYPE(FunctionImageChannel) args = { new_image, channels, function, nparms, parms, exception };
    CALL_FUNC_WITHOUT_GVL(GVL_FUNC(FunctionImageChannel), &args);
#endif
    xfree(parms);
    rm_check_exception(exception, new_image, DestroyOnError);
    DestroyExceptionInfo(exception);

    return rm_image_new(new_image);
}


/**
 * Get the number of algorithms search for a target color.
 * By default the color must be exact.
 * Use this attribute to match colors that are close to the target color in RGB space.
 *
 * @return [Float] the fuzz
 * @see Info#fuzz
 */
VALUE
Image_fuzz(VALUE self)
{
    IMPLEMENT_TYPED_ATTR_READER(Image, fuzz, dbl, &rm_image_data_type);
}


/**
 * Set the number of algorithms search for a target color.
 *
 * @param fuzz [String, Numeric] The argument may be a floating-point numeric value or a string in the
 *   form "NN%".
 * @return [String, Numeric] the given value
 * @see Info#fuzz=
 */
VALUE
Image_fuzz_eq(VALUE self, VALUE fuzz)
{
    Image *image = rm_check_frozen(self);
    image->fuzz = rm_fuzz_to_dbl(fuzz);
    return fuzz;
}


/**
 * Apply fx on the image.
 *
 * @overload fx(expression, channel = Magick::AllChannels)
 *   @param expression [String] A mathematical expression
 *   @param channel [Magick::ChannelType] a ChannelType arguments.
 *
 * @overload fx(expression, *channels)
 *   @param expression [String] A mathematical expression
 *   @param *channels [Magick::ChannelType] one or more ChannelType arguments.
 *
 * @return [Magick::Image] a new image
 */
VALUE
Image_fx(int argc, VALUE *argv, VALUE self)
{
    Image *image, *new_image;
    char *expression;
    ChannelType channels;
    ExceptionInfo *exception;

    image = rm_check_destroyed(self);
    channels = extract_channels(&argc, argv);

    // There must be exactly 1 remaining argument.
    if (argc == 0)
    {
        rb_raise(rb_eArgError, "wrong number of arguments (0 for 1 or more)");
    }
    else if (argc > 1)
    {
        raise_ChannelType_error(argv[argc-1]);
    }

    expression = StringValueCStr(argv[0]);

    exception = AcquireExceptionInfo();
#if defined(IMAGEMAGICK_7)
    BEGIN_CHANNEL_MASK(image, channels);
    GVL_STRUCT_TYPE(FxImage) args = { image, expression, exception };
    new_image = (Image *)CALL_FUNC_WITHOUT_GVL(GVL_FUNC(FxImage), &args);
    CHANGE_RESULT_CHANNEL_MASK(new_image);
    END_CHANNEL_MASK(image);
#else
    GVL_STRUCT_TYPE(FxImageChannel) args = { image, channels, expression, exception };
    new_image = (Image *)CALL_FUNC_WITHOUT_GVL(GVL_FUNC(FxImageChannel), &args);
#endif
    rm_check_exception(exception, new_image, DestroyOnError);
    DestroyExceptionInfo(exception);

    return rm_image_new(new_image);
}

/**
 * Get the gamma level of the image.
 *
 * @return [Float] the gamma level
 */
VALUE
Image_gamma(VALUE self)
{
    IMPLEMENT_TYPED_ATTR_READER(Image, gamma, dbl, &rm_image_data_type);
}

/**
 * Set the gamma level of the image.
 *
 * @param val [Numeric] the gamma level
 * @return [Numeric] the gamma level
 */
VALUE
Image_gamma_eq(VALUE self, VALUE val)
{
    IMPLEMENT_TYPED_ATTR_WRITER(Image, gamma, dbl, &rm_image_data_type);
}


/**
 * Apply gamma to a channel.
 *
 * @overload gamma_channel(gamma, channel = Magick::AllChannels)
 *   @param Values gamma [Numeric] typically range from 0.8 to 2.3. You can also reduce the influence
 *     of a particular channel with a gamma value of 0.
 *   @param channel [Magick::ChannelType] a ChannelType arguments.
 *
 * @overload gamma_channel(gamma, *channels)
 *   @param Values gamma [Numeric] typically range from 0.8 to 2.3. You can also reduce the influence
 *     of a particular channel with a gamma value of 0.
 *   @param *channels [Magick::ChannelType] one or more ChannelType arguments.
 *
 * @return [Magick::Image] a new image
 */
VALUE
Image_gamma_channel(int argc, VALUE *argv, VALUE self)
{
    Image *image, *new_image;
    ChannelType channels;
    double gamma;
#if defined(IMAGEMAGICK_7)
    ExceptionInfo *exception;
#endif

    image = rm_check_destroyed(self);
    channels = extract_channels(&argc, argv);

    // There must be exactly one remaining argument.
    if (argc == 0)
    {
        rb_raise(rb_eArgError, "missing gamma argument");
    }
    else if (argc > 1)
    {
        raise_ChannelType_error(argv[argc-1]);
    }

    gamma = NUM2DBL(argv[0]);
    new_image = rm_clone_image(image);

#if defined(IMAGEMAGICK_7)
    exception = AcquireExceptionInfo();
    BEGIN_CHANNEL_MASK(new_image, channels);
    GVL_STRUCT_TYPE(GammaImage) args = { new_image, gamma, exception };
    CALL_FUNC_WITHOUT_GVL(GVL_FUNC(GammaImage), &args);
    END_CHANNEL_MASK(new_image);
    rm_check_exception(exception, new_image, DestroyOnError);
    DestroyExceptionInfo(exception);
#else
    GVL_STRUCT_TYPE(GammaImageChannel) args = { new_image, channels, gamma };
    CALL_FUNC_WITHOUT_GVL(GVL_FUNC(GammaImageChannel), &args);
    rm_check_image_exception(new_image, DestroyOnError);
#endif

    return rm_image_new(new_image);
}


/**
 * gamma-correct an image.
 *
 * @overload gamma_correct(red_gamma, green_gamma = red_gamma, blue_gamma = green_gamma)
 *   @return [Magick::Image] a new image
 */
VALUE
Image_gamma_correct(int argc, VALUE *argv, VALUE self)
{
    Image *image, *new_image;
    double red_gamma, green_gamma, blue_gamma;
#if defined(IMAGEMAGICK_7)
    ExceptionInfo *exception;
#endif

    image = rm_check_destroyed(self);
    switch (argc)
    {
        case 1:
            red_gamma   = NUM2DBL(argv[0]);
            green_gamma = blue_gamma = red_gamma;
            break;
        case 2:
            red_gamma   = NUM2DBL(argv[0]);
            green_gamma = NUM2DBL(argv[1]);
            blue_gamma  = green_gamma;
            break;
        case 3:
        case 4:
            red_gamma     = NUM2DBL(argv[0]);
            green_gamma   = NUM2DBL(argv[1]);
            blue_gamma    = NUM2DBL(argv[2]);
            break;
        default:
            rb_raise(rb_eArgError, "wrong number of arguments (%d for 1 to 3)", argc);
            break;
    }

    new_image = rm_clone_image(image);

#if defined(IMAGEMAGICK_7)
    exception = AcquireExceptionInfo();
#endif

    if ((red_gamma == green_gamma) && (green_gamma == blue_gamma))
    {
#if defined(IMAGEMAGICK_7)
        BEGIN_CHANNEL_MASK(new_image, (ChannelType) (RedChannel | GreenChannel | BlueChannel));
        GVL_STRUCT_TYPE(GammaImage) args = { new_image, red_gamma, exception };
        CALL_FUNC_WITHOUT_GVL(GVL_FUNC(GammaImage), &args);
        END_CHANNEL_MASK(new_image);
#else
        GVL_STRUCT_TYPE(GammaImageChannel) args = { new_image, (ChannelType) (RedChannel | GreenChannel | BlueChannel), red_gamma };
        CALL_FUNC_WITHOUT_GVL(GVL_FUNC(GammaImageChannel), &args);
#endif
    }
    else
    {
#if defined(IMAGEMAGICK_7)
        BEGIN_CHANNEL_MASK(new_image, RedChannel);
        GVL_STRUCT_TYPE(GammaImage) args1 = { new_image, red_gamma, exception };
        CALL_FUNC_WITHOUT_GVL(GVL_FUNC(GammaImage), &args1);
        END_CHANNEL_MASK(new_image);

        BEGIN_CHANNEL_MASK(new_image, GreenChannel);
        GVL_STRUCT_TYPE(GammaImage) args2 = { new_image, green_gamma, exception };
        CALL_FUNC_WITHOUT_GVL(GVL_FUNC(GammaImage), &args2);
        END_CHANNEL_MASK(new_image);

        BEGIN_CHANNEL_MASK(new_image, BlueChannel);
        GVL_STRUCT_TYPE(GammaImage) args3 = { new_image, blue_gamma, exception };
        CALL_FUNC_WITHOUT_GVL(GVL_FUNC(GammaImage), &args3);
        END_CHANNEL_MASK(new_image);
#else
        GVL_STRUCT_TYPE(GammaImageChannel) args1 = { new_image, RedChannel, red_gamma };
        CALL_FUNC_WITHOUT_GVL(GVL_FUNC(GammaImageChannel), &args1);

        GVL_STRUCT_TYPE(GammaImageChannel) args2 = { new_image, GreenChannel, green_gamma };
        CALL_FUNC_WITHOUT_GVL(GVL_FUNC(GammaImageChannel), &args2);

        GVL_STRUCT_TYPE(GammaImageChannel) args3 = { new_image, BlueChannel, blue_gamma };
        CALL_FUNC_WITHOUT_GVL(GVL_FUNC(GammaImageChannel), &args3);
#endif
    }

#if defined(IMAGEMAGICK_7)
    rm_check_exception(exception, new_image, DestroyOnError);
    DestroyExceptionInfo(exception);
#else
    rm_check_image_exception(new_image, DestroyOnError);
#endif

    return rm_image_new(new_image);
}


/**
 * Blur the image.
 *
 * @overload gaussian_blur(radius = 0.0, sigma = 1.0)
 *   @param radius [Numeric] The radius of the Gaussian operator.
 *   @param sigma [Numeric] The sigma (standard deviation) of the Gaussian operator.
 *   @return [Magick::Image] a new image
 */
VALUE
Image_gaussian_blur(int argc, VALUE *argv, VALUE self)
{
    return effect_image(self, argc, argv, GVL_FUNC(GaussianBlurImage));
}


/**
 * Blur the image on a channel.
 *
 * @overload gaussian_blur_channel(radius = 0.0, sigma = 1.0, channel = Magick::AllChannels)
 *   @param radius [Numeric] The radius of the Gaussian operator.
 *   @param sigma [Numeric] The sigma (standard deviation) of the Gaussian operator.
 *   @param channel [Magick::ChannelType] a ChannelType arguments.
 *
 * @overload gaussian_blur_channel(radius = 0.0, sigma = 1.0, *channels)
 *   @param radius [Numeric] The radius of the Gaussian operator.
 *   @param sigma [Numeric] The sigma (standard deviation) of the Gaussian operator.
 *   @param *channels [Magick::ChannelType] one or more ChannelType arguments.
 *
 * @return [Magick::Image] a new image
 */
VALUE
Image_gaussian_blur_channel(int argc, VALUE *argv, VALUE self)
{
    Image *image, *new_image;
    ChannelType channels;
    ExceptionInfo *exception;
    double radius = 0.0, sigma = 1.0;

    image = rm_check_destroyed(self);
    channels = extract_channels(&argc, argv);

    // There can be 0, 1, or 2 remaining arguments.
    switch (argc)
    {
        case 2:
            sigma = NUM2DBL(argv[1]);
            /* Fall thru */
        case 1:
            radius = NUM2DBL(argv[0]);
            /* Fall thru */
        case 0:
            break;
        default:
            raise_ChannelType_error(argv[argc-1]);
    }

    exception = AcquireExceptionInfo();
#if defined(IMAGEMAGICK_7)
    BEGIN_CHANNEL_MASK(image, channels);
    GVL_STRUCT_TYPE(GaussianBlurImage) args = { image, radius, sigma, exception };
    new_image = (Image *)CALL_FUNC_WITHOUT_GVL(GVL_FUNC(GaussianBlurImage), &args);
    CHANGE_RESULT_CHANNEL_MASK(new_image);
    END_CHANNEL_MASK(image);
    rm_check_exception(exception, new_image, DestroyOnError);
#else
    GVL_STRUCT_TYPE(GaussianBlurImageChannel) args = { image, channels, radius, sigma, exception };
    new_image = (Image *)CALL_FUNC_WITHOUT_GVL(GVL_FUNC(GaussianBlurImageChannel), &args);
    rm_check_exception(exception, new_image, DestroyOnError);
#endif

    DestroyExceptionInfo(exception);

    return rm_image_new(new_image);
}


/**
 * Get the preferred size of the image when encoding.
 *
 * @return [String] the geometry
 * @see https://www.imagemagick.org/Magick++/Geometry.html
 */
VALUE
Image_geometry(VALUE self)
{
    IMPLEMENT_TYPED_ATTR_READER(Image, geometry, str, &rm_image_data_type);
}


/**
 * Set the preferred size of the image when encoding.
 *
 * @param geometry [Magick::Geometry, String] the geometry
 * @return [Magick::Geometry, String] the given geometry
 * @see https://www.imagemagick.org/Magick++/Geometry.html
 */
VALUE
Image_geometry_eq(VALUE self, VALUE geometry)
{
    Image *image;
    VALUE geom_str;
    char *geom;

    image = rm_check_frozen(self);

    if (geometry == Qnil)
    {
        magick_free(image->geometry);
        image->geometry = NULL;
        return self;
    }


    geom_str = rb_String(geometry);
    geom = StringValueCStr(geom_str);
    if (!IsGeometry(geom))
    {
        rb_raise(rb_eTypeError, "invalid geometry: %s", geom);
    }
    magick_clone_string(&image->geometry, geom);

    RB_GC_GUARD(geom_str);

    return geometry;
}


/**
 * Gets the pixels from the specified rectangle within the image.
 *
 * @param x_arg [Numeric] x position of start of region
 * @param y_arg [Numeric] y position of start of region
 * @param cols_arg [Numeric] width of region
 * @param rows_arg [Numeric] height of region
 * @return [Array<Magick::Pixel>] An array of Magick::Pixel objects corresponding to the pixels in the rectangle
 *   defined by the geometry parameters.
 * @see Image#store_pixels
 */
VALUE
Image_get_pixels(VALUE self, VALUE x_arg, VALUE y_arg, VALUE cols_arg, VALUE rows_arg)
{
    Image *image;
    ExceptionInfo *exception;
    long x, y;
    unsigned long columns, rows;
    long size, n;
    VALUE pixel_ary;
#if defined(IMAGEMAGICK_7)
    const Quantum *pixels;
#else
    const PixelPacket *pixels;
    const IndexPacket *indexes;
#endif

    image = rm_check_destroyed(self);
    x       = NUM2LONG(x_arg);
    y       = NUM2LONG(y_arg);
    columns = NUM2ULONG(cols_arg);
    rows    = NUM2ULONG(rows_arg);

    if ((x+columns) > image->columns || (y+rows) > image->rows)
    {
        rb_raise(rb_eRangeError, "geometry (%lux%lu%+ld%+ld) exceeds image bounds",
                 columns, rows, x, y);
    }

    // Cast AcquireImagePixels to get rid of the const qualifier. We're not going
    // to change the pixels but I don't want to make "pixels" const.
    exception = AcquireExceptionInfo();
    GVL_STRUCT_TYPE(GetVirtualPixels) args = { image, x, y, columns, rows, exception };
    void *ret = CALL_FUNC_WITHOUT_GVL(GVL_FUNC(GetVirtualPixels), &args);
    pixels = reinterpret_cast<decltype(pixels)>(ret);
    CHECK_EXCEPTION();

    DestroyExceptionInfo(exception);

    // If the function failed, return a 0-length array.
    if (!pixels)
    {
        return rb_ary_new();
    }

    // Allocate an array big enough to contain the PixelPackets.
    size = (long)(columns * rows);
    pixel_ary = rb_ary_new2(size);

#if defined(IMAGEMAGICK_6)
    indexes = GetVirtualIndexQueue(image);
#endif

    // Convert the PixelPackets to Magick::Pixel objects
    for (n = 0; n < size; n++)
    {
#if defined(IMAGEMAGICK_7)
        PixelPacket color;
        memset(&color, 0, sizeof(color));
        color.red   = GetPixelRed(image, pixels);
        color.green = GetPixelGreen(image, pixels);
        color.blue  = GetPixelBlue(image, pixels);
        color.alpha = GetPixelAlpha(image, pixels);
        color.black = GetPixelBlack(image, pixels);
        rb_ary_store(pixel_ary, n, Pixel_from_PixelPacket(&color));

        pixels += GetPixelChannels(image);
#else
        MagickPixel mpp;
        mpp.red = GetPixelRed(pixels);
        mpp.green = GetPixelGreen(pixels);
        mpp.blue = GetPixelBlue(pixels);
        mpp.opacity = GetPixelOpacity(pixels);
        if (indexes)
        {
            mpp.index = GetPixelIndex(indexes + n);
        }
        rb_ary_store(pixel_ary, n, Pixel_from_MagickPixel(&mpp));
        pixels++;
#endif
    }

    return pixel_ary;
}


/**
 * Run a function testing whether this image has an attribute.
 *
 * No Ruby usage (internal function)
 *
 * @param self this object
 * @param attr_test the attribute testing function
 * @return the result of attr_test.
 */
static VALUE
has_attribute(VALUE self, MagickBooleanType (attr_test)(const Image *, ExceptionInfo *))
{
    Image *image;
    ExceptionInfo *exception;
    MagickBooleanType r;

    image = rm_check_destroyed(self);
    exception = AcquireExceptionInfo();

    r = (attr_test)(image, exception);
    CHECK_EXCEPTION();
    DestroyExceptionInfo(exception);

    return r ? Qtrue : Qfalse;
}


#if defined(IMAGEMAGICK_7)
/**
 * Run a function testing whether this image has an attribute.
 *
 * No Ruby usage (internal function)
 *
 * @param self this object
 * @param attr_test the attribute testing function
 * @return the result of attr_test.
 */
static VALUE
has_image_attribute(VALUE self, MagickBooleanType (attr_test)(const Image *))
{
    Image *image;
    MagickBooleanType r;

    image = rm_check_destroyed(self);
    r = (attr_test)(image);

    return r ? Qtrue : Qfalse;
}
#endif

static MagickBooleanType
Image_is_gray_colorspace_type(const Image *image, ExceptionInfo *exception)
{
    ColorspaceType colorspace = GetImageColorspaceType(image, exception);
    return (colorspace == GRAYColorspace || colorspace == LinearGRAYColorspace) ? MagickTrue : MagickFalse;
}


/**
 * Return true if all the pixels in the image have the same red, green, and blue intensities.
 *
 * @return [Boolean] true if image is gray, false otherwise
 */
VALUE
Image_gray_q(VALUE self)
{
    return has_attribute(self, Image_is_gray_colorspace_type);
}


/**
 * Return true if has 1024 unique colors or less.
 *
 * @return [Boolean] true if image has <= 1024 unique colors
 */
VALUE
Image_histogram_q(VALUE self)
{
    return has_attribute(self, IsHistogramImage);
}


/**
 * Implode the image by the specified percentage.
 *
 * @overload implode(amount = 0.50)
 *   @param amount [Numeric] Increasing the absolute value of the argument increases the effect.
 *     The value may be positive for implosion, or negative for explosion. The default is 0.50.
 *   @return [Magick::Image] a new image
 */
VALUE
Image_implode(int argc, VALUE *argv, VALUE self)
{
    Image *image, *new_image;
    double amount = 0.50;
    ExceptionInfo *exception;

    switch (argc)
    {
        case 1:
            amount = NUM2DBL(argv[0]);
        case 0:
            break;
        default:
            rb_raise(rb_eArgError, "wrong number of arguments (%d for 0 or 1)", argc);
    }

    image = rm_check_destroyed(self);
    exception = AcquireExceptionInfo();

#if defined(IMAGEMAGICK_7)
    GVL_STRUCT_TYPE(ImplodeImage) args = { image, amount, image->interpolate, exception };
#else
    GVL_STRUCT_TYPE(ImplodeImage) args = { image, amount, exception };
#endif
    new_image = (Image *)CALL_FUNC_WITHOUT_GVL(GVL_FUNC(ImplodeImage), &args);
    rm_check_exception(exception, new_image, DestroyOnError);
    DestroyExceptionInfo(exception);

    return rm_image_new(new_image);
}


/**
 * Store image pixel data from an array.
 *
 * @overload store_pixels(x, y, columns, rows, map, pixels, type = Magick::CharPixel)
 *   @param x [Numeric] The x-offset of the rectangle to be replaced.
 *   @param y [Numeric] The y-offset of the rectangle to be replaced.
 *   @param columns [Numeric] The number of columns in the rectangle.
 *   @param rows [Numeric] The number of rows in the rectangle.
 *   @param map [String] his string reflects the expected ordering of the pixel array.
 *   @param pixels [Array] An array of pixels.
 *     The number of pixels in the array must be the same as the number
 *     of pixels in the rectangle, that is, rows*columns.
 *   @param type [Magick::StorageType] A StorageType value that specifies the C datatype to which
 *     the pixel data will be converted.
 *   @return [Magick::Image] self
 *   @see Image#export_pixels
 */
VALUE
Image_import_pixels(int argc, VALUE *argv, VALUE self)
{
    Image *image;
    long x_off, y_off;
    unsigned long cols, rows;
    unsigned long n, npixels;
    size_t buffer_l;
    char *map;
    VALUE pixel_arg, pixel_ary;
    StorageType stg_type = CharPixel;
    size_t type_sz, map_l;
    Quantum *pixels = NULL;
    double *fpixels = NULL;
    void *buffer;
    MagickBooleanType okay;
#if defined(IMAGEMAGICK_7)
    ExceptionInfo *exception;
#endif

    image = rm_check_frozen(self);

    switch (argc)
    {
        case 7:
            VALUE_TO_ENUM(argv[6], stg_type, StorageType);
        case 6:
            x_off = NUM2LONG(argv[0]);
            y_off = NUM2LONG(argv[1]);
            cols = NUM2ULONG(argv[2]);
            rows = NUM2ULONG(argv[3]);
            map = StringValueCStr(argv[4]);
            pixel_arg = argv[5];
            break;
        default:
            rb_raise(rb_eArgError, "wrong number of arguments (%d for 6 or 7)", argc);
            break;
    }

    if (x_off < 0 || y_off < 0 || cols <= 0 || rows <= 0)
    {
        rb_raise(rb_eArgError, "invalid import geometry");
    }

    map_l = rm_strnlen_s(map, MaxTextExtent);
    npixels = cols * rows * map_l;

    // Assume that any object that responds to :to_str is a string buffer containing
    // binary pixel data.
    if (rb_respond_to(pixel_arg, rb_intern("to_str")))
    {
        buffer = (void *)rm_str2cstr(pixel_arg, &buffer_l);
        switch (stg_type)
        {
            case CharPixel:
                type_sz = 1;
                break;
            case ShortPixel:
                type_sz = sizeof(unsigned short);
                break;
            case LongPixel:
                type_sz = sizeof(unsigned long);
                break;
            case DoublePixel:
                type_sz = sizeof(double);
                break;
            case FloatPixel:
                type_sz = sizeof(float);
                break;
            case QuantumPixel:
                type_sz = sizeof(Quantum);
                break;
            default:
                rb_raise(rb_eArgError, "unsupported storage type %s", StorageType_name(stg_type));
                break;
        }

        if (buffer_l % type_sz != 0)
        {
            rb_raise(rb_eArgError, "pixel buffer must be an exact multiple of the storage type size");
        }
        if ((buffer_l / type_sz) % map_l != 0)
        {
            rb_raise(rb_eArgError, "pixel buffer must contain an exact multiple of the map length");
        }
        if ((unsigned long)(buffer_l / type_sz) < npixels)
        {
            rb_raise(rb_eArgError, "pixel buffer too small (need %lu channel values, got %" RMIuSIZE ")",
                     npixels, buffer_l/type_sz);
        }
    }
    // Otherwise convert the argument to an array and convert the array elements
    // to binary pixel data.
    else
    {
        // rb_Array converts an object that is not an array to an array if possible,
        // and raises TypeError if it can't. It usually is possible.
        pixel_ary = rb_Array(pixel_arg);

        if (RARRAY_LEN(pixel_ary) % map_l != 0)
        {
            rb_raise(rb_eArgError, "pixel array must contain an exact multiple of the map length");
        }
        if ((unsigned long)RARRAY_LEN(pixel_ary) < npixels)
        {
            rb_raise(rb_eArgError, "pixel array too small (need %lu elements, got %ld)",
                     npixels, RARRAY_LEN(pixel_ary));
        }

        if (stg_type == DoublePixel || stg_type == FloatPixel)
        {
            fpixels = ALLOC_N(double, npixels);
            for (n = 0; n < npixels; n++)
            {
                VALUE element = rb_ary_entry(pixel_ary, n);
                if (rm_check_num2dbl(element))
                {
                    fpixels[n] = NUM2DBL(element);
                }
                else
                {
                    xfree(fpixels);
                    rb_raise(rb_eTypeError, "type mismatch: %s given", rb_class2name(CLASS_OF(element)));
                }
            }
            buffer = (void *) fpixels;
            stg_type = DoublePixel;
        }
        else
        {
            pixels = ALLOC_N(Quantum, npixels);
            for (n = 0; n < npixels; n++)
            {
                VALUE element = rb_ary_entry(pixel_ary, n);
                if (rm_check_num2dbl(element))
                {
                    pixels[n] = NUM2DBL(element);
                }
                else
                {
                    xfree(pixels);
                    rb_raise(rb_eTypeError, "type mismatch: %s given", rb_class2name(CLASS_OF(element)));
                }
            }
            buffer = (void *) pixels;
            stg_type = QuantumPixel;
        }
    }


#if defined(IMAGEMAGICK_7)
    exception = AcquireExceptionInfo();
    GVL_STRUCT_TYPE(ImportImagePixels) args = { image, x_off, y_off, cols, rows, map, stg_type, buffer, exception };
#else
    GVL_STRUCT_TYPE(ImportImagePixels) args = { image, x_off, y_off, cols, rows, map, stg_type, buffer };
#endif
    void *ret = CALL_FUNC_WITHOUT_GVL(GVL_FUNC(ImportImagePixels), &args);
    okay = static_cast<MagickBooleanType>(reinterpret_cast<intptr_t &>(ret));

    // Free pixel array before checking for errors.
    if (pixels)
    {
        xfree((void *)pixels);
    }
    if (fpixels)
    {
        xfree((void *)fpixels);
    }

    if (!okay)
    {
#if defined(IMAGEMAGICK_7)
        CHECK_EXCEPTION();
        DestroyExceptionInfo(exception);
#else
        rm_check_image_exception(image, RetainOnError);
#endif
        // Shouldn't get here...
        rm_magick_error("ImportImagePixels failed with no explanation.");
    }
#if defined(IMAGEMAGICK_7)
    DestroyExceptionInfo(exception);
#endif

    RB_GC_GUARD(pixel_arg);
    RB_GC_GUARD(pixel_ary);

    return self;
}


/**
 * Override Object#inspect - return a string description of the image.
 *
 * No Ruby usage (internal function)
 *
 * Notes:
 *   - This is essentially the IdentifyImage except the description is built in
 *     a char buffer instead of being written to a file.
 *
 * @param image the image to inspect
 * @param buffer buffer for the output string
 * @param len length of buffer
 * @see Image_inspect
 */
static void
build_inspect_string(Image *image, char *buffer, size_t len)
{
    unsigned long quantum_depth;
    int x = 0;                  // # bytes used in buffer

    // Print magick filename if different from current filename.
    if (*image->magick_filename != '\0' && strcmp(image->magick_filename, image->filename) != 0)
    {
        x += snprintf(buffer+x, len-x, "%.1024s=>", image->magick_filename);
    }
    // Print current filename.
    x += snprintf(buffer+x, len-x, "%.1024s", image->filename);
    // Print scene number.
    if ((GetPreviousImageInList(image) != NULL) && (GetNextImageInList(image) != NULL) && image->scene > 0)
    {
        x += snprintf(buffer+x, len-x, "[%" RMIuSIZE "]", image->scene);
    }
    // Print format
    x += snprintf(buffer+x, len-x, " %s ", image->magick);

    // Print magick columnsXrows if different from current.
    if (image->magick_columns != 0 || image->magick_rows != 0)
    {
        if (image->magick_columns != image->columns || image->magick_rows != image->rows)
        {
            x += snprintf(buffer+x, len-x, "%" RMIuSIZE "x%" RMIuSIZE "=>", image->magick_columns, image->magick_rows);
        }
    }

    x += snprintf(buffer+x, len-x, "%" RMIuSIZE "x%" RMIuSIZE " ", image->columns, image->rows);

    // Print current columnsXrows
    if (   image->page.width != 0 || image->page.height != 0
           || image->page.x != 0     || image->page.y != 0)
    {
        x += snprintf(buffer+x, len-x, "%" RMIuSIZE "x%" RMIuSIZE "+%" RMIdSIZE "+%" RMIdSIZE " ",
                      image->page.width, image->page.height,
                      image->page.x, image->page.y);
    }

    if (image->storage_class == DirectClass)
    {
        x += snprintf(buffer+x, len-x, "DirectClass ");
        if (image->total_colors != 0)
        {
            if (image->total_colors >= (unsigned long)(1 << 24))
            {
                x += snprintf(buffer+x, len-x, "%" RMIuSIZE "mc ", image->total_colors/1024/1024);
            }
            else
            {
                if (image->total_colors >= (unsigned long)(1 << 16))
                {
                    x += snprintf(buffer+x, len-x, "%" RMIuSIZE "kc ", image->total_colors/1024);
                }
                else
                {
                    x += snprintf(buffer+x, len-x, "%" RMIuSIZE "c ", image->total_colors);
                }
            }
        }
    }
    else
    {
        // Cast `image->colors' to long to suppress gcc warnings when
        // building with GM. GM defines that field as an unsigned int.
        if (image->total_colors <= image->colors)
        {
            x += snprintf(buffer+x, len-x, "PseudoClass %ldc ", (long) image->colors);
        }
        else
        {
            x += snprintf(buffer+x, len-x, "PseudoClass %" RMIuSIZE "=>%" RMIuSIZE "c ", image->total_colors, image->colors);
            if (image->error.mean_error_per_pixel != 0.0)
            {
                x += snprintf(buffer+x, len-x, "%ld/%.6f/%.6fdb ",
                              (long) (image->error.mean_error_per_pixel+0.5),
                              image->error.normalized_mean_error,
                              image->error.normalized_maximum_error);
            }
        }
    }

    // Print bit depth
    quantum_depth = GetImageQuantumDepth(image, MagickTrue);
    x += snprintf(buffer+x, len-x, "%lu-bit", quantum_depth);

    // Print blob info if appropriate.
    if (GetBlobSize(image) != 0)
    {
        if (GetBlobSize(image) >= (1 << 24))
        {
            x += snprintf(buffer+x, len-x, " %lumb", (unsigned long) (GetBlobSize(image)/1024/1024));
        }
        else if (GetBlobSize(image) >= 1024)
        {
            x += snprintf(buffer+x, len-x, " %lukb", (unsigned long) (GetBlobSize(image)/1024));
        }
        else
        {
            x += snprintf(buffer+x, len-x, " %lub", (unsigned long) GetBlobSize(image));
        }
    }


    if (len-1-x > 6)
    {
        size_t value_l;
        const char *value = GetImageArtifact(image, "user");
        if (value)
        {
            strcpy(buffer+x, " user:");
            x += 6;
            value_l = len - x - 1;
            value_l = min(rm_strnlen_s(value, MaxTextExtent), value_l);
            memcpy(buffer+x, value, value_l);
            x += value_l;
        }
    }

    assert(x < (int)(len-1));
    buffer[x] = '\0';

    return;
}


/**
 * Override {Object#inspect} - return a string description of the image.
 *
 * @return [String] the string
 */
VALUE
Image_inspect(VALUE self)
{
    Image *image;
    char buffer[MaxTextExtent];          // image description buffer

    TypedData_Get_Struct(self, Image, &rm_image_data_type, image);
    if (!image)
    {
        return rb_str_new2("#<Magick::Image: (destroyed)>");
    }
    build_inspect_string(image, buffer, sizeof(buffer));
    return rb_str_new2(buffer);
}


/**
 * Get the type of interlacing scheme (default NoInterlace).
 * This option is used to specify the type of interlacing scheme for raw image formats such as RGB
 * or YUV.
 * NoInterlace means do not interlace, LineInterlace uses scanline interlacing, and PlaneInterlace
 * uses plane interlacing. PartitionInterlace is like PlaneInterlace except the different planes are
 * saved to individual files (e.g. image.R, image.G, and image.B).
 *
 * @return [Magick::InterlaceType] the interlace
 */
VALUE
Image_interlace(VALUE self)
{
    Image *image = rm_check_destroyed(self);
    return InterlaceType_find(image->interlace);
}


/**
 * Set the type of interlacing scheme.
 *
 * @param interlace [Magick::InterlaceType] the interlace
 * @return [Magick::InterlaceType] the given value
 */
VALUE
Image_interlace_eq(VALUE self, VALUE interlace)
{
    Image *image = rm_check_frozen(self);
    VALUE_TO_ENUM(interlace, image->interlace, InterlaceType);
    return interlace;
}


/**
 * Return the IPTC profile as a String.
 *
 * @return [String, nil] the IPTC profile if it exists, otherwise nil
 */
VALUE
Image_iptc_profile(VALUE self)
{
    Image *image;
    const StringInfo *profile;

    image = rm_check_destroyed(self);
    profile = GetImageProfile(image, "iptc");
    if (!profile)
    {
        return Qnil;
    }

    return rb_str_new((char *)profile->datum, (long)profile->length);

}



/**
 * Set the IPTC profile. The argument is a string.
 *
 * @param profile [String, nil] the IPTC profile
 * @return [String, nil] the given profile
 */
VALUE
Image_iptc_profile_eq(VALUE self, VALUE profile)
{
    Image_delete_profile(self, rb_str_new2("iptc"));
    if (profile != Qnil)
    {
        set_profile(self, "iptc", profile);
    }
    return profile;
}


/*
 *  These are undocumented methods. The writer is
 *  called only by Image#iterations=.
 *  The reader is only used by the unit tests!
 */
VALUE
Image_iterations(VALUE self)
{
    IMPLEMENT_TYPED_ATTR_READER(Image, iterations, int, &rm_image_data_type);
}
VALUE
Image_iterations_eq(VALUE self, VALUE val)
{
    IMPLEMENT_TYPED_ATTR_WRITER(Image, iterations, int, &rm_image_data_type);
}

/**
 * Adjusts the levels of an image by scaling the colors falling between specified white and black
 * points to the full available quantum range.
 *
 * @overload level2(black_point = 0.0, white_point = Magick::QuantumRange, gamma = 1.0)
 *   @param black_point [Numeric] A black point level in the range 0 - QuantumRange.
 *   @param white_point [Numeric] A white point level in the range 0..QuantumRange.
 *   @param gamma [Numeric] A gamma correction in the range 0.0 - 10.0.
 *   @return [Magick::Image] a new image
 */
VALUE
Image_level2(int argc, VALUE *argv, VALUE self)
{
    Image *image, *new_image;
    double black_point = 0.0, gamma_val = 1.0, white_point = (double)QuantumRange;
#if defined(IMAGEMAGICK_7)
    ExceptionInfo *exception;
#else
    char level[50];
#endif

    image = rm_check_destroyed(self);
    switch (argc)
    {
        case 0:             // take all the defaults
            break;
        case 1:
            black_point = NUM2DBL(argv[0]);
            white_point = QuantumRange - black_point;
            break;
        case 2:
            black_point = NUM2DBL(argv[0]);
            white_point = NUM2DBL(argv[1]);
            break;
        case 3:
            black_point = NUM2DBL(argv[0]);
            white_point = NUM2DBL(argv[1]);
            gamma_val   = NUM2DBL(argv[2]);
            break;
        default:
            rb_raise(rb_eArgError, "wrong number of arguments (%d for 0 to 3)", argc);
            break;
    }

    new_image = rm_clone_image(image);

#if defined(IMAGEMAGICK_7)
    exception = AcquireExceptionInfo();
    GVL_STRUCT_TYPE(LevelImage) args = { new_image, black_point, white_point, gamma_val, exception };
    CALL_FUNC_WITHOUT_GVL(GVL_FUNC(LevelImage), &args);
    rm_check_exception(exception, new_image, DestroyOnError);
    DestroyExceptionInfo(exception);
#else
    snprintf(level, sizeof(level), "%gx%g+%g", black_point, white_point, gamma_val);
    GVL_STRUCT_TYPE(LevelImage) args = { new_image, level };
    CALL_FUNC_WITHOUT_GVL(GVL_FUNC(LevelImage), &args);
    rm_check_image_exception(new_image, DestroyOnError);
#endif

    return rm_image_new(new_image);
}


/**
 * Similar to {Image#level2} but applies to a single channel only.
 *
 * @overload level_channel(aChannelType, black = 0.0, white = 1.0, gamma = Magick::QuantumRange)
 *   @param aChannelType [Magick::ChannelType] A ChannelType value.
 *   @param black [Numeric] A black point level in the range 0..QuantumRange.
 *   @param white [Numeric] A white point level in the range 0..QuantumRange.
 *   @param gamma [Numeric] A gamma correction in the range 0.0 - 10.0.
 *   @return [Magick::Image] a new image
 *   @see Image#level2
 */
VALUE
Image_level_channel(int argc, VALUE *argv, VALUE self)
{
    Image *image, *new_image;
    double black_point = 0.0, gamma_val = 1.0, white_point = (double)QuantumRange;
    ChannelType channel;
#if defined(IMAGEMAGICK_7)
    ExceptionInfo *exception;
#endif

    image = rm_check_destroyed(self);
    switch (argc)
    {
        case 1:             // take all the defaults
            break;
        case 2:
            black_point = NUM2DBL(argv[1]);
            white_point = QuantumRange - black_point;
            break;
        case 3:
            black_point = NUM2DBL(argv[1]);
            white_point = NUM2DBL(argv[2]);
            break;
        case 4:
            black_point = NUM2DBL(argv[1]);
            white_point = NUM2DBL(argv[2]);
            gamma_val   = NUM2DBL(argv[3]);
            break;
        default:
            rb_raise(rb_eArgError, "wrong number of arguments (%d for 1 to 4)", argc);
            break;
    }

    VALUE_TO_ENUM(argv[0], channel, ChannelType);

    new_image = rm_clone_image(image);

#if defined(IMAGEMAGICK_7)
    exception = AcquireExceptionInfo();
    BEGIN_CHANNEL_MASK(new_image, channel);
    GVL_STRUCT_TYPE(LevelImage) args = { new_image, black_point, white_point, gamma_val, exception };
    CALL_FUNC_WITHOUT_GVL(GVL_FUNC(LevelImage), &args);
    END_CHANNEL_MASK(new_image);
    rm_check_exception(exception, new_image, DestroyOnError);
    DestroyExceptionInfo(exception);
#else
    GVL_STRUCT_TYPE(LevelImageChannel) args = { new_image, channel, black_point, white_point, gamma_val };
    CALL_FUNC_WITHOUT_GVL(GVL_FUNC(LevelImageChannel), &args);
    rm_check_image_exception(new_image, DestroyOnError);
#endif

    return rm_image_new(new_image);
}


/**
 * When invert is true, black and white will be mapped to the black_color and white_color colors,
 * compressing all other colors linearly. When invert is false, black and white will be mapped to
 * the black_color and white_color colors, stretching all other colors linearly.
 *
 * @overload level_colors(black_color = "black", white_color = "white", invert = true, channel = Magick::AllChannels)
 *   @param black_color [Magick::Pixel, String] The color to be mapped to black
 *   @param white_color [Magick::Pixel, String] The color to be mapped to white
 *   @param invert See the description above
 *   @param channel [Magick::ChannelType] a ChannelType arguments.
 *
 * @overload level_colors(black_color = "black", white_color = "white", invert = true, *channels)
 *   @param black_color [Magick::Pixel, String] The color to be mapped to black
 *   @param white_color [Magick::Pixel, String] The color to be mapped to white
 *   @param invert See the description above
 *   @param *channels [Magick::ChannelType] one or more ChannelType arguments.
 *
 * @return [Magick::Image] a new image
 */
VALUE
Image_level_colors(int argc, VALUE *argv, VALUE self)
{
    Image *image, *new_image;
    MagickPixel black_color, white_color;
    ChannelType channels;
    MagickBooleanType invert = MagickTrue;
    MagickBooleanType okay;
#if defined(IMAGEMAGICK_7)
    ExceptionInfo *exception;
#endif

    image = rm_check_destroyed(self);

    channels = extract_channels(&argc, argv);

    rm_init_magickpixel(image, &white_color);
    rm_init_magickpixel(image, &black_color);

    switch (argc)
    {
        case 3:
            invert = (MagickBooleanType)RTEST(argv[2]);

        case 2:
            Color_to_MagickPixel(image, &white_color, argv[1]);
            Color_to_MagickPixel(image, &black_color, argv[0]);
            break;

        case 1:
            rm_set_magickpixel(&white_color, "white");
            Color_to_MagickPixel(image, &black_color, argv[0]);
            break;

        case 0:
            rm_set_magickpixel(&white_color, "white");
            rm_set_magickpixel(&black_color, "black");
            break;

        default:
            raise_ChannelType_error(argv[argc-1]);
            break;
    }

    new_image = rm_clone_image(image);

#if defined(IMAGEMAGICK_7)
    exception = AcquireExceptionInfo();
    BEGIN_CHANNEL_MASK(new_image, channels);
    GVL_STRUCT_TYPE(LevelImageColors) args = { new_image, &black_color, &white_color, invert, exception };
    void *ret = CALL_FUNC_WITHOUT_GVL(GVL_FUNC(LevelImageColors), &args);
    okay = static_cast<MagickBooleanType>(reinterpret_cast<intptr_t &>(ret));
    END_CHANNEL_MASK(new_image);
    rm_check_exception(exception, new_image, DestroyOnError);
    DestroyExceptionInfo(exception);
#else
    GVL_STRUCT_TYPE(LevelColorsImageChannel) args = { new_image, channels, &black_color, &white_color, invert };
    void *ret = CALL_FUNC_WITHOUT_GVL(GVL_FUNC(LevelColorsImageChannel), &args);
    okay = static_cast<MagickBooleanType>(reinterpret_cast<intptr_t &>(ret));
    rm_check_image_exception(new_image, DestroyOnError);
#endif
    if (!okay)
    {
        DestroyImage(new_image);
        rb_raise(rb_eRuntimeError, "LevelImageColors failed for unknown reason.");
    }

    return rm_image_new(new_image);
}



/**
 * Maps black and white to the specified points. The reverse of {Image#level_channel}.
 *
 * @overload levelize_channel(black_point, white_point = Magick::QuantumRange - black_point, gamma = 1.0, channel = Magick::AllChannels)
 *   @param black [Numeric] A black point level in the range 0..QuantumRange.
 *   @param white [Numeric] A white point level in the range 0..QuantumRange.
 *   @param gamma [Numeric] A gamma correction in the range 0.0 - 10.0.
 *   @param channel [Magick::ChannelType] a ChannelType arguments.
 *
 * @overload levelize_channel(black_point, white_point = Magick::QuantumRange - black_point, gamma = 1.0, *channels)
 *   @param black [Numeric] A black point level in the range 0..QuantumRange.
 *   @param white [Numeric] A white point level in the range 0..QuantumRange.
 *   @param gamma [Numeric] A gamma correction in the range 0.0 - 10.0.
 *   @param *channels [Magick::ChannelType] one or more ChannelType arguments.
 *
 * @return [Magick::Image] a new image
 */
VALUE
Image_levelize_channel(int argc, VALUE *argv, VALUE self)
{
    Image *image, *new_image;
    ChannelType channels;
    double black_point, white_point;
    double gamma = 1.0;
    MagickBooleanType okay;
#if defined(IMAGEMAGICK_7)
    ExceptionInfo *exception;
#endif

    image = rm_check_destroyed(self);
    channels = extract_channels(&argc, argv);
    if (argc > 3)
    {
        raise_ChannelType_error(argv[argc-1]);
    }

    switch (argc)
    {
        case 3:
            gamma = NUM2DBL(argv[2]);
        case 2:
            white_point = NUM2DBL(argv[1]);
            black_point = NUM2DBL(argv[0]);
            break;
        case 1:
            black_point = NUM2DBL(argv[0]);
            white_point = QuantumRange - black_point;
            break;
        default:
            rb_raise(rb_eArgError, "wrong number of arguments (%d for 1 or more)", argc);
            break;
    }

    new_image = rm_clone_image(image);

#if defined(IMAGEMAGICK_7)
    exception = AcquireExceptionInfo();
    BEGIN_CHANNEL_MASK(new_image, channels);
    GVL_STRUCT_TYPE(LevelizeImage) args = { new_image, black_point, white_point, gamma, exception };
    void *ret = CALL_FUNC_WITHOUT_GVL(GVL_FUNC(LevelizeImage), &args);
    okay = static_cast<MagickBooleanType>(reinterpret_cast<intptr_t &>(ret));
    END_CHANNEL_MASK(new_image);
    rm_check_exception(exception, new_image, DestroyOnError);
    DestroyExceptionInfo(exception);
#else
    GVL_STRUCT_TYPE(LevelizeImageChannel) args = { new_image, channels, black_point, white_point, gamma };
    void *ret = CALL_FUNC_WITHOUT_GVL(GVL_FUNC(LevelizeImageChannel), &args);
    okay = static_cast<MagickBooleanType>(reinterpret_cast<intptr_t &>(ret));
    rm_check_image_exception(new_image, DestroyOnError);
#endif

    if (!okay)
    {
        DestroyImage(new_image);
        rb_raise(rb_eRuntimeError, "LevelizeImageChannel failed for unknown reason.");
    }
    return rm_image_new(new_image);
}


/**
 * Linear with saturation stretch.
 *
 * @overload linear_stretch(black_point, white_point = pixels - black_point)
 *   @param black_point [Numeric, String] black out at most this many pixels.
 *     Specify an absolute number of pixels as a numeric value, or a percentage as a string in the
 *     form 'NN%'.
 *   @param white_point [Numeric, String] burn at most this many pixels.
 *     Specify an absolute number of pixels as a numeric value, or a percentage as a string in the
 *     form 'NN%'.
 *     This argument is optional. If not specified the default is `(columns * rows) - black_point`.
 *   @return [Magick::Image] a new image
 *   @see Image#contrast_stretch_channel
 */
VALUE
Image_linear_stretch(int argc, VALUE *argv, VALUE self)
{
    Image *image, *new_image;
    double black_point, white_point;
#if defined(IMAGEMAGICK_7)
    ExceptionInfo *exception;
#endif

    image = rm_check_destroyed(self);
    get_black_white_point(image, argc, argv, &black_point, &white_point);
    new_image = rm_clone_image(image);

#if defined(IMAGEMAGICK_7)
    exception = AcquireExceptionInfo();
    GVL_STRUCT_TYPE(LinearStretchImage) args = { new_image, black_point, white_point, exception };
    CALL_FUNC_WITHOUT_GVL(GVL_FUNC(LinearStretchImage), &args);
    rm_check_exception(exception, new_image, DestroyOnError);
    DestroyExceptionInfo(exception);
#else
    GVL_STRUCT_TYPE(LinearStretchImage) args = { new_image, black_point, white_point };
    CALL_FUNC_WITHOUT_GVL(GVL_FUNC(LinearStretchImage), &args);
    rm_check_image_exception(new_image, DestroyOnError);
#endif

    return rm_image_new(new_image);
}


/**
 * Rescale image with seam carving.
 *
 * @overload liquid_rescale(columns, rows, delta_x = 0.0, rigidity = 0.0)
 *   @param columns [Numeric] The desired width height. Should not exceed 200% of the original
 *     dimension.
 *   @param rows [Numeric] The desired height. Should not exceed 200% of the original dimension.
 *   @param delta_x [Numeric] Maximum seam transversal step (0 means straight seams).
 *   @param rigidity [Numeric] Introduce a bias for non-straight seams (typically 0).
 *   @return [Magick::Image] a new image
 */
VALUE
Image_liquid_rescale(int argc, VALUE *argv, VALUE self)
{
    Image *image, *new_image;
    unsigned long cols, rows;
    double delta_x = 0.0;
    double rigidity = 0.0;
    ExceptionInfo *exception;

    image = rm_check_destroyed(self);

    switch (argc)
    {
        case 4:
            rigidity = NUM2DBL(argv[3]);
        case 3:
            delta_x = NUM2DBL(argv[2]);
        case 2:
            rows = NUM2ULONG(argv[1]);
            cols = NUM2ULONG(argv[0]);
            break;
        default:
            rb_raise(rb_eArgError, "wrong number of arguments (%d for 2 to 4)", argc);
            break;
    }

    exception = AcquireExceptionInfo();
    GVL_STRUCT_TYPE(LiquidRescaleImage) args = { image, cols, rows, delta_x, rigidity, exception };
    new_image = (Image *)CALL_FUNC_WITHOUT_GVL(GVL_FUNC(LiquidRescaleImage), &args);
    rm_check_exception(exception, new_image, DestroyOnError);
    DestroyExceptionInfo(exception);

    return rm_image_new(new_image);
}


/**
 * Implement marshalling.
 *
 * @param str [String] the marshalled string
 * @return [Magic::Image] a new image
 * @see Image#_dump
 */
VALUE
Image__load(VALUE klass ATTRIBUTE_UNUSED, VALUE str)
{
    Image *image;
    ImageInfo *info;
    DumpedImage mi;
    ExceptionInfo *exception;
    char *blob;
    size_t length;

    blob = rm_str2cstr(str, &length);

    // Must be as least as big as the 1st 4 fields in DumpedImage
    if (length <= (long)(sizeof(DumpedImage)-MaxTextExtent))
    {
        rb_raise(rb_eTypeError, "image is invalid or corrupted (too short)");
    }

    // Retrieve & validate the image format from the header portion
    mi.id = ((DumpedImage *)blob)->id;
    if (mi.id != DUMPED_IMAGE_ID)
    {
        rb_raise(rb_eTypeError, "image is invalid or corrupted (invalid header)");
    }

    mi.mj = ((DumpedImage *)blob)->mj;
    mi.mi = ((DumpedImage *)blob)->mi;
    if (   mi.mj != DUMPED_IMAGE_MAJOR_VERS
           || mi.mi > DUMPED_IMAGE_MINOR_VERS)
    {
        rb_raise(rb_eTypeError, "incompatible image format (can't be read)\n"
                 "\tformat version %d.%d required; %d.%d given",
                 DUMPED_IMAGE_MAJOR_VERS, DUMPED_IMAGE_MINOR_VERS,
                 mi.mj, mi.mi);
    }

    mi.len = ((DumpedImage *)blob)->len;

    // Must be bigger than the header
    if (length <= (mi.len + sizeof(DumpedImage) - MaxTextExtent))
    {
        rb_raise(rb_eTypeError, "image is invalid or corrupted (too short)");
    }

    info = CloneImageInfo(NULL);

    memcpy(info->magick, ((DumpedImage *)blob)->magick, mi.len);
    info->magick[mi.len] = '\0';

    exception = AcquireExceptionInfo();

    blob += offsetof(DumpedImage, magick) + mi.len;
    length -= offsetof(DumpedImage, magick) + mi.len;
    GVL_STRUCT_TYPE(BlobToImage) args = { info, blob, (size_t)length, exception };
    image = (Image *)CALL_FUNC_WITHOUT_GVL(GVL_FUNC(BlobToImage), &args);
    DestroyImageInfo(info);

    rm_check_exception(exception, image, DestroyOnError);
    DestroyExceptionInfo(exception);

    return rm_image_new(image);
}


// aliases for common use of structure types; MagnifyImage, MinifyImage
typedef GVL_STRUCT_TYPE(MagnifyImage) GVL_STRUCT_TYPE(magnify);

/**
 * Scale an image proportionally to twice its size.
 *
 * No Ruby usage (internal function)
 *
 * @param bang whether the bang (!) version of the method was called
 * @param self this object
 * @param magnifier function to use for magnification
 * @return self if bang, otherwise a new image
 */
static VALUE
magnify(int bang, VALUE self, gvl_function_t fp)
{
    Image *image;
    Image *new_image;
    ExceptionInfo *exception;

    TypedData_Get_Struct(self, Image, &rm_image_data_type, image);
    exception = AcquireExceptionInfo();

    GVL_STRUCT_TYPE(magnify) args = { image, exception };
    new_image = (Image *)CALL_FUNC_WITHOUT_GVL(fp, &args);
    rm_check_exception(exception, new_image, DestroyOnError);

    DestroyExceptionInfo(exception);

    if (bang)
    {
        rm_ensure_result(new_image);
        UPDATE_DATA_PTR(self, new_image);
        rm_image_destroy(image);
        return self;
    }

    return rm_image_new(new_image);
}


/**
 * Scale an image proportionally to twice its size.
 *
 * @return [Magick::Image] a new image
 * @see Image#magnify!
 */
VALUE
Image_magnify(VALUE self)
{
    rm_check_destroyed(self);
    return magnify(False, self, GVL_FUNC(MagnifyImage));
}


/**
 * Scale an image proportionally to twice its size.
 * In-place form of {Image#magnify}.
 *
 * @return [Magick::Image] self
 * @see Image#magnify
 */
VALUE
Image_magnify_bang(VALUE self)
{
    rm_check_frozen(self);
    return magnify(True, self, GVL_FUNC(MagnifyImage));
}


/**
 * Support Marshal.dump.
 *
 * @return [Array<String>] The first element in the array is the file name. The second element is the string
 *   of blob.
 */
VALUE
Image_marshal_dump(VALUE self)
{
    Image *image;
    Info *info;
    unsigned char *blob;
    size_t length;
    VALUE ary;
    ExceptionInfo *exception;

    image = rm_check_destroyed(self);

    info = CloneImageInfo(NULL);
    if (!info)
    {
        rb_raise(rb_eNoMemError, "not enough memory to initialize Info object");
    }

    ary = rb_ary_new2(2);
    rb_ary_store(ary, 0, rb_str_new2(image->filename));

    exception = AcquireExceptionInfo();
    GVL_STRUCT_TYPE(ImageToBlob) args = { info, image, &length, exception };
    blob = (unsigned char *)CALL_FUNC_WITHOUT_GVL(GVL_FUNC(ImageToBlob), &args);

    // Destroy info before raising an exception
    DestroyImageInfo(info);
    CHECK_EXCEPTION();
    DestroyExceptionInfo(exception);

    rb_ary_store(ary, 1, rb_str_new((char *)blob, (long)length));
    magick_free((void*)blob);

    return ary;
}


/**
 * Support Marshal.load.
 *
 * @param ary [Array<String>] the array returned from {Image#marshal_dump}
 * @return self
 */
VALUE
Image_marshal_load(VALUE self, VALUE ary)
{
    VALUE blob, filename;
    Info *info;
    Image *image, *new_image;
    ExceptionInfo *exception;

    TypedData_Get_Struct(self, Image, &rm_image_data_type, image);

    filename = rb_ary_shift(ary);
    blob = rb_ary_shift(ary);

    filename = StringValue(filename);
    blob = StringValue(blob);

    info = CloneImageInfo(NULL);
    if (!info)
    {
        rb_raise(rb_eNoMemError, "not enough memory to initialize Info object");
    }

    exception = AcquireExceptionInfo();
    if (filename != Qnil)
    {
        strlcpy(info->filename, RSTRING_PTR(filename), sizeof(info->filename));
    }
    GVL_STRUCT_TYPE(BlobToImage) args = { info, RSTRING_PTR(blob), (size_t)RSTRING_LEN(blob), exception };
    new_image = (Image *)CALL_FUNC_WITHOUT_GVL(GVL_FUNC(BlobToImage), &args);

    // Destroy info before raising an exception
    DestroyImageInfo(info);
    CHECK_EXCEPTION();
    DestroyExceptionInfo(exception);

    UPDATE_DATA_PTR(self, new_image);
    rm_image_destroy(image);

    return self;
}

/**
 * Return the image's clip mask, or nil if it doesn't have a clip mask.
 *
 * No Ruby usage (internal function)
 *
 * Notes:
 *   - Distinguish from Image#clip_mask
 *
 * @param image the image
 * @return copy of the current clip-mask or nil
 */
static VALUE
get_image_mask(Image *image)
{
    Image *mask;
    ExceptionInfo *exception;

    exception = AcquireExceptionInfo();

    // The returned clip mask is a clone, ours to keep.
#if defined(IMAGEMAGICK_7)
    GVL_STRUCT_TYPE(GetImageMask) args = { image, WritePixelMask, exception };
    mask = (Image *)CALL_FUNC_WITHOUT_GVL(GVL_FUNC(GetImageMask), &args);
#else
    GVL_STRUCT_TYPE(GetImageClipMask) args = { image, exception };
    mask = (Image *)CALL_FUNC_WITHOUT_GVL(GVL_FUNC(GetImageClipMask), &args);
#endif
    rm_check_exception(exception, mask, DestroyOnError);

    DestroyExceptionInfo(exception);

    return mask ? rm_image_new(mask) : Qnil;
}

/**
 * Associate a mask with the image.
 *
 * No Ruby usage (internal function)
 *
 * @param image the image
 * @param mask the mask
 * @return copy of the current clip-mask or nil
 * @see get_image_mask
 */
#if defined(IMAGEMAGICK_7)
static VALUE
set_image_mask(Image *image, VALUE mask)
{
    Image *mask_image, *resized_image;
    Image *clip_mask;
    ExceptionInfo *exception;

    exception = AcquireExceptionInfo();

    if (mask != Qnil)
    {
        mask = rm_cur_image(mask);
        mask_image = rm_check_destroyed(mask);
        clip_mask = rm_clone_image(mask_image);

        // Resize if necessary
        if (clip_mask->columns != image->columns || clip_mask->rows != image->rows)
        {
            GVL_STRUCT_TYPE(ResizeImage) args = { clip_mask, image->columns, image->rows, image->filter, exception };
            resized_image = (Image *)CALL_FUNC_WITHOUT_GVL(GVL_FUNC(ResizeImage), &args);
            DestroyImage(clip_mask);
            rm_check_exception(exception, resized_image, DestroyOnError);
            rm_ensure_result(resized_image);
            clip_mask = resized_image;
        }

        GVL_STRUCT_TYPE(SetImageMask) args = { image, WritePixelMask, clip_mask, exception };
        CALL_FUNC_WITHOUT_GVL(GVL_FUNC(SetImageMask), &args);
        DestroyImage(clip_mask);
    }
    else
    {
        GVL_STRUCT_TYPE(SetImageMask) args = { image, WritePixelMask, NULL, exception };
        CALL_FUNC_WITHOUT_GVL(GVL_FUNC(SetImageMask), &args);
    }
    CHECK_EXCEPTION();
    DestroyExceptionInfo(exception);

    // Always return a copy of the mask!
    return get_image_mask(image);
}
#else
static VALUE
set_image_mask(Image *image, VALUE mask)
{
    Image *mask_image, *resized_image;
    Image *clip_mask;
    long x, y;
    PixelPacket *q;
    ExceptionInfo *exception;

    if (mask != Qnil)
    {
        mask = rm_cur_image(mask);
        mask_image = rm_check_destroyed(mask);
        clip_mask = rm_clone_image(mask_image);

        // Resize if necessary
        if (clip_mask->columns != image->columns || clip_mask->rows != image->rows)
        {
            exception = AcquireExceptionInfo();
            GVL_STRUCT_TYPE(ResizeImage) args = { clip_mask, image->columns, image->rows, UndefinedFilter, 0.0, exception };
            resized_image = (Image *)CALL_FUNC_WITHOUT_GVL(GVL_FUNC(ResizeImage), &args);
            rm_check_exception(exception, resized_image, DestroyOnError);
            DestroyExceptionInfo(exception);
            rm_ensure_result(resized_image);
            DestroyImage(clip_mask);
            clip_mask = resized_image;
        }

        // The following section is copied from mogrify.c (6.2.8-8)
        exception = AcquireExceptionInfo();

        for (y = 0; y < (long) clip_mask->rows; y++)
        {
            GVL_STRUCT_TYPE(GetAuthenticPixels) args_GetAuthenticPixels = { clip_mask, 0, y, clip_mask->columns, 1, exception };
            q = (PixelPacket *)CALL_FUNC_WITHOUT_GVL(GVL_FUNC(GetAuthenticPixels), &args_GetAuthenticPixels);
            rm_check_exception(exception, clip_mask, DestroyOnError);

            if (!q)
            {
                break;
            }
            for (x = 0; x < (long) clip_mask->columns; x++)
            {
                if (clip_mask->matte == MagickFalse)
                {
                    q->opacity = PIXEL_INTENSITY(q);
                }
                q->red = q->opacity;
                q->green = q->opacity;
                q->blue = q->opacity;
                q += 1;
            }

            GVL_STRUCT_TYPE(SyncAuthenticPixels) args_SyncAuthenticPixels = { clip_mask, exception };
            CALL_FUNC_WITHOUT_GVL(GVL_FUNC(SyncAuthenticPixels), &args_SyncAuthenticPixels);
            rm_check_exception(exception, clip_mask, DestroyOnError);
        }
        DestroyExceptionInfo(exception);

        GVL_STRUCT_TYPE(SetImageStorageClass) args_SetImageStorageClass = { clip_mask, DirectClass };
        CALL_FUNC_WITHOUT_GVL(GVL_FUNC(SetImageStorageClass), &args_SetImageStorageClass);
        rm_check_image_exception(clip_mask, DestroyOnError);

        clip_mask->matte = MagickTrue;

        // SetImageClipMask clones the clip_mask image. We can
        // destroy our copy after SetImageClipMask is done with it.

        GVL_STRUCT_TYPE(SetImageClipMask) args_SetImageClipMask = { image, clip_mask };
        CALL_FUNC_WITHOUT_GVL(GVL_FUNC(SetImageClipMask), &args_SetImageClipMask);
        DestroyImage(clip_mask);
    }
    else
    {
        GVL_STRUCT_TYPE(SetImageClipMask) args_SetImageClipMask = { image, NULL };
        CALL_FUNC_WITHOUT_GVL(GVL_FUNC(SetImageClipMask), &args_SetImageClipMask);
    }

    RB_GC_GUARD(mask);

    // Always return a copy of the mask!
    return get_image_mask(image);
}
#endif


/**
 * Get/Sets an image clip mask created from the specified mask image.
 * The mask image must have the same dimensions as the image being masked.
 * If not, the mask image is resized to match. If the mask image has an alpha channel the opacity of
 * each pixel is used to define the mask. Otherwise, the intensity (gray level) of each pixel is
 * used.
 *
 * In general, if the mask image does not have an alpha channel, a white pixel in the mask prevents
 * changes to the corresponding pixel in the image being masked, while a black pixel allows changes.
 * A pixel that is neither black nor white will allow partial changes depending on its intensity.
 *
 * @overload mask()
 *   Get an image clip mask.
 *
 * @overload mask(image)
 *   Set an image clip mask.
 *   @param image [Magick::Image, Magick::ImageList] Either an imagelist or an image. If an
 *     imagelist, uses the current image.
 *
 * @return [Magick::Image, nil] the mask image
 */
VALUE
Image_mask(int argc, VALUE *argv, VALUE self)
{
    VALUE mask;
    Image *image;

    image = rm_check_destroyed(self);
    if (argc == 0)
    {
        return get_image_mask(image);
    }
    if (argc > 1)
    {
        rb_raise(rb_eArgError, "wrong number of arguments (expected 0 or 1, got %d)", argc);
    }

    rb_check_frozen(self);
    mask = argv[0];
    return set_image_mask(image, mask);
}


/**
 * Return the matte color.
 *
 * @return [String] the matte color
 */
VALUE
Image_matte_color(VALUE self)
{
    Image *image = rm_check_destroyed(self);
    return rm_pixelcolor_to_color_name(image, &image->matte_color);
}

/**
 * Set the matte color.
 *
 * @param color [Magick::Pixel, String] the matte color
 * @return [Magick::Pixel, String] the given color
 */
VALUE
Image_matte_color_eq(VALUE self, VALUE color)
{
    Image *image = rm_check_frozen(self);
    Color_to_PixelColor(&image->matte_color, color);
    return color;
}


/**
 * Makes transparent all the pixels that are the same color as the pixel at x, y, and are neighbors.
 *
 * @overload Image#matte_flood_fill(color, x, y, method_obj, alpha:)
 *   @param color [Magick::Pixel, String] the color name
 *   @param x_obj [Numeric] x position
 *   @param y_obj [Numeric] y position
 *   @param method_obj [Magick::PaintMethod] which method to call: FloodfillMethod or FillToBorderMethod
 *   @param alpha [Numeric] the alpha
 *   @return [Magick::Image] a new image
 */
VALUE
Image_matte_flood_fill(int argc, VALUE *argv, VALUE self)
{
    Image *image, *new_image;
    PixelColor target;
    Quantum alpha;
    long x, y;
    PaintMethod method;
    DrawInfo *draw_info;
    MagickPixel target_mpp;
    MagickBooleanType invert;
#if defined(IMAGEMAGICK_7)
    ExceptionInfo *exception;
#endif
    VALUE color_obj, x_obj, y_obj, method_obj, kwargs;

    image = rm_check_destroyed(self);

    rb_scan_args(argc, argv, "4:", &color_obj, &x_obj, &y_obj, &method_obj, &kwargs);

    alpha = get_named_alpha_value(kwargs);

    Color_to_PixelColor(&target, color_obj);
    VALUE_TO_ENUM(method_obj, method, PaintMethod);
    if (!(method == FloodfillMethod || method == FillToBorderMethod))
    {
        rb_raise(rb_eArgError, "paint method_obj must be FloodfillMethod or "
                 "FillToBorderMethod (%d given)", method);
    }
    x = NUM2LONG(x_obj);
    y = NUM2LONG(y_obj);
    if ((unsigned long)x > image->columns || (unsigned long)y > image->rows)
    {
        rb_raise(rb_eArgError, "target out of range. %ldx%ld given, image is %" RMIuSIZE "x%" RMIuSIZE "",
                 x, y, image->columns, image->rows);
    }


    new_image = rm_clone_image(image);

    // FloodfillPaintImage looks for the opacity in the DrawInfo.fill field.
    draw_info = CloneDrawInfo(NULL, NULL);
    if (!draw_info)
    {
        rb_raise(rb_eNoMemError, "not enough memory to continue");
    }
#if defined(IMAGEMAGICK_7)
    rm_set_pixelinfo_alpha(&draw_info->fill, alpha);
#else
    draw_info->fill.opacity = QuantumRange - alpha;
#endif

    if (method == FillToBorderMethod)
    {
        invert = MagickTrue;
        target_mpp.red   = (MagickRealType) image->border_color.red;
        target_mpp.green = (MagickRealType) image->border_color.green;
        target_mpp.blue  = (MagickRealType) image->border_color.blue;
#if defined(IMAGEMAGICK_7)
        rm_set_pixelinfo_alpha(&target_mpp, (MagickRealType) image->border_color.alpha);
#else
        target_mpp.opacity = (MagickRealType) image->border_color.opacity;
#endif
    }
    else
    {
        invert = MagickFalse;
        target_mpp.red   = (MagickRealType) target.red;
        target_mpp.green = (MagickRealType) target.green;
        target_mpp.blue  = (MagickRealType) target.blue;
#if defined(IMAGEMAGICK_7)
        rm_set_pixelinfo_alpha(&target_mpp, (MagickRealType) target.alpha);
#else
        target_mpp.opacity = (MagickRealType) target.opacity;
#endif
    }

#if defined(IMAGEMAGICK_7)
    exception = AcquireExceptionInfo();
    BEGIN_CHANNEL_MASK(new_image, OpacityChannel);
    GVL_STRUCT_TYPE(FloodfillPaintImage) args = { new_image, draw_info, &target_mpp, x, y, invert, exception };
    CALL_FUNC_WITHOUT_GVL(GVL_FUNC(FloodfillPaintImage), &args);
    END_CHANNEL_MASK(new_image);
    DestroyDrawInfo(draw_info);
    rm_check_exception(exception, new_image, DestroyOnError);
    DestroyExceptionInfo(exception);
#else
    GVL_STRUCT_TYPE(FloodfillPaintImage) args = { new_image, OpacityChannel, draw_info, &target_mpp, x, y, invert };
    CALL_FUNC_WITHOUT_GVL(GVL_FUNC(FloodfillPaintImage), &args);
    DestroyDrawInfo(draw_info);

    rm_check_image_exception(new_image, DestroyOnError);
#endif

    return rm_image_new(new_image);
}


/**
 * Apply a digital filter that improves the quality of a noisy image. Each pixel is replaced by the
 * median in a set of neighboring pixels as defined by radius.
 *
 * @overload median_filter(radius = 0.0)
 *   @param radius [Numeric] The filter radius.
 *   @return [Magick::Image] a new image
 */
VALUE
Image_median_filter(int argc, VALUE *argv, VALUE self)
{
    Image *image, *new_image;
    double radius = 0.0;
    ExceptionInfo *exception;

    image = rm_check_destroyed(self);
    switch (argc)
    {
        case 1:
            radius = NUM2DBL(argv[0]);
        case 0:
            break;
        default:
            rb_raise(rb_eArgError, "wrong number of arguments (%d for 0 or 1)", argc);
            break;
    }

    exception = AcquireExceptionInfo();
    GVL_STRUCT_TYPE(StatisticImage) args = { image, MedianStatistic, (size_t)radius, (size_t)radius, exception };
    new_image = (Image *)CALL_FUNC_WITHOUT_GVL(GVL_FUNC(StatisticImage), &args);
    rm_check_exception(exception, new_image, DestroyOnError);
    DestroyExceptionInfo(exception);

    return rm_image_new(new_image);
}


/**
 * Get the mean error per pixel computed when a image is color reduced.
 *
 * @return [Float] the mean error per pixel
 */
VALUE
Image_mean_error_per_pixel(VALUE self)
{
    IMPLEMENT_TYPED_ATTR_READERF(Image, mean_error_per_pixel, error.mean_error_per_pixel, dbl, &rm_image_data_type);
}


/**
 * Return the officially registered (or de facto) MIME media-type corresponding to the image format.
 *
 * @return [String, nil] the mime type
 */
VALUE
Image_mime_type(VALUE self)
{
    Image *image;
    char *type;
    VALUE mime_type;

    image = rm_check_destroyed(self);
    type = MagickToMime(image->magick);
    if (!type)
    {
        return Qnil;
    }
    mime_type = rb_str_new2(type);

    // The returned string must be deallocated by the user.
    magick_free(type);

    RB_GC_GUARD(mime_type);

    return mime_type;
}


/**
 * Scale an image proportionally to half its size.
 *
 * @return [Magick::Image] a new image
 * @see Image#minify!
 */
VALUE
Image_minify(VALUE self)
{
    rm_check_destroyed(self);
    return magnify(False, self, GVL_FUNC(MinifyImage));
}


/**
 * Scale an image proportionally to half its size.  In-place form of {Image#minify}.
 *
 * @return [Magick::Image] self
 * @see Image#minify
 */
VALUE
Image_minify_bang(VALUE self)
{
    rm_check_frozen(self);
    return magnify(True, self, GVL_FUNC(MinifyImage));
}


/**
 * Changes the brightness, saturation, and hue.
 *
 * @overload modulate(brightness = 1.0, saturation = 1.0, hue = 1.0)
 *   @param brightness [Numeric, String] The percent change in the brightness.
 *     Must be a non-negative number or a string in the form "NN%".
 *   @param saturation [Numeric, String] The percent change in the saturation.
 *     Must be a number or a string in the form "NN%".
 *   @param hue [Numeric, String] The percent change in the hue.
 *     Must be a number or a string in the form "NN%".
 *   @return [Magick::Image] a new image
 */
VALUE
Image_modulate(int argc, VALUE *argv, VALUE self)
{
    Image *image, *new_image;
    double pct_brightness = 100.0,
    pct_saturation = 100.0,
    pct_hue        = 100.0;
    char modulate[100];
#if defined(IMAGEMAGICK_7)
    ExceptionInfo *exception;
#endif

    image = rm_check_destroyed(self);
    switch (argc)
    {
        case 3:
            pct_hue        = rm_percentage2(argv[2], 1.0, false) * 100.0;
        case 2:
            pct_saturation = rm_percentage2(argv[1], 1.0, false) * 100.0;
        case 1:
            pct_brightness = rm_percentage(argv[0], 1.0) * 100.0;
            break;
        case 0:
            break;
        default:
            rb_raise(rb_eArgError, "wrong number of arguments (%d for 0 to 3)", argc);
            break;
    }

    if (pct_brightness <= 0.0)
    {
        rb_raise(rb_eArgError, "brightness is %g%%, must be positive", pct_brightness);
    }
    snprintf(modulate, sizeof(modulate), "%f%%,%f%%,%f%%", pct_brightness, pct_saturation, pct_hue);

    new_image = rm_clone_image(image);

#if defined(IMAGEMAGICK_7)
    exception = AcquireExceptionInfo();
    GVL_STRUCT_TYPE(ModulateImage) args = { new_image, modulate, exception };
    CALL_FUNC_WITHOUT_GVL(GVL_FUNC(ModulateImage), &args);
    rm_check_exception(exception, new_image, DestroyOnError);
    DestroyExceptionInfo(exception);
#else
    GVL_STRUCT_TYPE(ModulateImage) args = { new_image, modulate };
    CALL_FUNC_WITHOUT_GVL(GVL_FUNC(ModulateImage), &args);
    rm_check_image_exception(new_image, DestroyOnError);
#endif

    return rm_image_new(new_image);
}


/**
 * Return true if all the pixels in the image have the same red, green, and blue intensities and the
 * intensity is either 0 or {Magick::QuantumRange}.
 *
 * @return [Boolean] true if monochrome, false otherwise
 */
VALUE
Image_monochrome_q(VALUE self)
{
#if defined(IMAGEMAGICK_7)
    return has_image_attribute(self, IsImageMonochrome);
#else
    return has_attribute(self, IsMonochromeImage);
#endif
}


/**
 * Tile size and offset within an image montage. Only valid for montage images.
 *
 * @return [String, nil] the tile size and offset
 */
VALUE
Image_montage(VALUE self)
{
    IMPLEMENT_TYPED_ATTR_READER(Image, montage, str, &rm_image_data_type);
}


// aliases for common use of structure types; MotionBlurImage, SketchImage
typedef GVL_STRUCT_TYPE(MotionBlurImage) GVL_STRUCT_TYPE(motion_blur);

/**
 * Called from Image_motion_blur and Image_sketch.
 *
 * No Ruby usage (internal function)
 *
 * @param argc number of input arguments
 * @param argv array of input arguments
 * @param self this object
 * @param fp the blur function to call
 * @return a new image
 * @see Image_motion_blur
 * @see Image_sketch
 */
static VALUE
motion_blur(int argc, VALUE *argv, VALUE self, gvl_function_t fp)
{
    Image *image, *new_image;
    double radius = 0.0;
    double sigma = 1.0;
    double angle = 0.0;
    ExceptionInfo *exception;

    switch (argc)
    {
        case 3:
            angle = NUM2DBL(argv[2]);
        case 2:
            sigma = NUM2DBL(argv[1]);
        case 1:
            radius = NUM2DBL(argv[0]);
        case 0:
            break;
        default:
            rb_raise(rb_eArgError, "wrong number of arguments (%d for 0 to 3)", argc);
            break;
    }

    if (sigma == 0.0)
    {
        rb_raise(rb_eArgError, "sigma must be != 0.0");
    }

    TypedData_Get_Struct(self, Image, &rm_image_data_type, image);

    exception = AcquireExceptionInfo();
    GVL_STRUCT_TYPE(motion_blur) args = { image, radius, sigma, angle, exception };
    new_image = (Image *)CALL_FUNC_WITHOUT_GVL(fp, &args);
    rm_check_exception(exception, new_image, DestroyOnError);
    DestroyExceptionInfo(exception);

    return rm_image_new(new_image);
}


/**
 * Simulate motion blur. Convolve the image with a Gaussian operator of the given radius and
 * standard deviation (sigma). For reasonable results, radius should be larger than sigma. Use a
 * radius of 0 and motion_blur selects a suitable radius for you. Angle gives the angle of the
 * blurring motion.
 *
 * @overload motion_blur(radius = 0.0, sigma = 1.0, angle = 0.0)
 *   @param radius [Numeric] The radius
 *   @param sigma [Numeric] The standard deviation
 *   @param angle [Numeric] The angle (in degrees)
 *   @return [Magick::Image] a new image
 */
VALUE
Image_motion_blur(int argc, VALUE *argv, VALUE self)
{
    rm_check_destroyed(self);
    return motion_blur(argc, argv, self, GVL_FUNC(MotionBlurImage));
}


/**
 * Negate the colors in the reference image. The grayscale option means that only grayscale values
 * within the image are negated.
 *
 * @overload negate(grayscale = false)
 *   @param grayscale [Boolean] If the grayscale argument is true, only the grayscale values are negated.
 *   @return [Magick::Image] a new image
 */
VALUE
Image_negate(int argc, VALUE *argv, VALUE self)
{
    Image *image, *new_image;
    MagickBooleanType grayscale = MagickFalse;
#if defined(IMAGEMAGICK_7)
    ExceptionInfo *exception;
#endif

    image = rm_check_destroyed(self);
    if (argc == 1)
    {
        grayscale = (MagickBooleanType)RTEST(argv[0]);
    }
    else if (argc > 1)
    {
        rb_raise(rb_eArgError, "wrong number of arguments (%d for 0 or 1)", argc);
    }

    new_image = rm_clone_image(image);

#if defined(IMAGEMAGICK_7)
    exception = AcquireExceptionInfo();
    GVL_STRUCT_TYPE(NegateImage) args = { new_image, grayscale, exception };
    CALL_FUNC_WITHOUT_GVL(GVL_FUNC(NegateImage), &args);
    rm_check_exception(exception, new_image, DestroyOnError);
    DestroyExceptionInfo(exception);
#else
    GVL_STRUCT_TYPE(NegateImage) args = { new_image, grayscale };
    CALL_FUNC_WITHOUT_GVL(GVL_FUNC(NegateImage), &args);
    rm_check_image_exception(new_image, DestroyOnError);
#endif

    return rm_image_new(new_image);
}


/**
 * Negate the colors on a particular channel. The grayscale option means that
 * only grayscale values within the image are negated.
 *
 * @overload negate_channel(grayscale = false, channel = Magick::AllChannels)
 *   @param grayscale [Boolean] If the grayscale argument is true, only the grayscale values are
 *     negated.
 *   @param channel [Magick::ChannelType] a ChannelType arguments.
 *
 * @overload negate_channel(grayscale = false, *channels)
 *   @param grayscale [Boolean] If the grayscale argument is true, only the grayscale values are
 *     negated.
 *   @param *channels [Magick::ChannelType] one or more ChannelType arguments.
 *
 * @return [Magick::Image] a new image
 */
VALUE
Image_negate_channel(int argc, VALUE *argv, VALUE self)
{
    Image *image, *new_image;
    ChannelType channels;
    MagickBooleanType grayscale = MagickFalse;
#if defined(IMAGEMAGICK_7)
    ExceptionInfo *exception;
#endif

    image = rm_check_destroyed(self);
    channels = extract_channels(&argc, argv);

    // There can be at most 1 remaining argument.
    if (argc > 1)
    {
        raise_ChannelType_error(argv[argc-1]);
    }
    else if (argc == 1)
    {
        grayscale = (MagickBooleanType)RTEST(argv[0]);
    }

    new_image = rm_clone_image(image);

#if defined(IMAGEMAGICK_7)
    exception = AcquireExceptionInfo();
    BEGIN_CHANNEL_MASK(new_image, channels);
    GVL_STRUCT_TYPE(NegateImage) args = { new_image, grayscale, exception };
    CALL_FUNC_WITHOUT_GVL(GVL_FUNC(NegateImage), &args);
    END_CHANNEL_MASK(new_image);
    rm_check_exception(exception, new_image, DestroyOnError);
    DestroyExceptionInfo(exception);
#else
    GVL_STRUCT_TYPE(NegateImageChannel) args = { new_image, channels, grayscale };
    CALL_FUNC_WITHOUT_GVL(GVL_FUNC(NegateImageChannel), &args);
    rm_check_image_exception(new_image, DestroyOnError);
#endif

    return rm_image_new(new_image);
}


/**
 * "Allocate" a new Image object
 *
 * @return [Magick::Image] a newly allocated image
 */
VALUE
Image_alloc(VALUE klass)
{
    VALUE image_obj;

    image_obj = TypedData_Wrap_Struct(klass, &rm_image_data_type, NULL);

    RB_GC_GUARD(image_obj);

    return image_obj;
}

/**
 * Initialize a new Image object If the fill argument is omitted, fill with background color.
 *
 * @overload initialize(cols, rows, fill = nil)
 *   @param cols [Numeric] the image width
 *   @param rows [Numeric] the image height
 *   @param fill [Magick::HatchFill, Magick::SolidFill] if object is given as fill argument,
 *     background color will be filled using it.
 *   @return [Magick::Image] self
 */
VALUE
Image_initialize(int argc, VALUE *argv, VALUE self)
{
    VALUE fill = Qnil;
    Info *info;
    VALUE info_obj;
    Image *image;
    unsigned long cols, rows;
#if defined(IMAGEMAGICK_7)
    ExceptionInfo *exception;
#endif

    switch (argc)
    {
        case 3:
            fill = argv[2];
        case 2:
            rows = NUM2ULONG(argv[1]);
            cols = NUM2ULONG(argv[0]);
            break;
        default:
            rb_raise(rb_eArgError, "wrong number of arguments (%d for 2 or 3)", argc);
            break;
    }

    // Create a new Info object to use when creating this image.
    info_obj = rm_info_new();
    TypedData_Get_Struct(info_obj, Info, &rm_info_data_type, info);

    image = rm_acquire_image(info);
    if (!image)
    {
        rb_raise(rb_eNoMemError, "not enough memory to continue");
    }

    rm_set_user_artifact(image, info);

    // NOW store a real image in the image object.
    UPDATE_DATA_PTR(self, image);

#if defined(IMAGEMAGICK_7)
    exception = AcquireExceptionInfo();
    GVL_STRUCT_TYPE(SetImageExtent) args = { image, cols, rows, exception };
    CALL_FUNC_WITHOUT_GVL(GVL_FUNC(SetImageExtent), &args);
    CHECK_EXCEPTION();
    DestroyExceptionInfo(exception);
#else
    GVL_STRUCT_TYPE(SetImageExtent) args = { image, cols, rows };
    CALL_FUNC_WITHOUT_GVL(GVL_FUNC(SetImageExtent), &args);
#endif

    rm_sync_image_options(image, info);

    // If the caller did not supply a fill argument, call SetImageBackgroundColor
    // to fill the image using the background color. The background color can
    // be set by specifying it when creating the Info parm block.
    if (NIL_P(fill))
    {
#if defined(IMAGEMAGICK_7)
        exception = AcquireExceptionInfo();
        GVL_STRUCT_TYPE(SetImageBackgroundColor) args = { image, exception };
        CALL_FUNC_WITHOUT_GVL(GVL_FUNC(SetImageBackgroundColor), &args);
        CHECK_EXCEPTION();
        DestroyExceptionInfo(exception);
#else
        GVL_STRUCT_TYPE(SetImageBackgroundColor) args = { image };
        CALL_FUNC_WITHOUT_GVL(GVL_FUNC(SetImageBackgroundColor), &args);
#endif
    }
    // fillobj.fill(self)
    else
    {
        rb_funcall(fill, rm_ID_fill, 1, self);
    }

    RB_GC_GUARD(fill);
    RB_GC_GUARD(info_obj);

    return self;
}


/**
 * Create a new Image object from an Image structure.
 *
 * No Ruby usage (internal function)
 *
 * Notes:
 *   - Since the Image is already created we don't need to call Image_alloc or
 *     Image_initialize.
 *
 * @param image the Image structure
 * @return a new image
 */
VALUE
rm_image_new(Image *image)
{
    rm_ensure_result(image);

    return TypedData_Wrap_Struct(Class_Image, &rm_image_data_type, image);
}


/**
 * Enhance the contrast of a color image by adjusting the pixels color to span the entire range of
 * colors available.
 *
 * @return [Magick::Image] a new image
 */
VALUE
Image_normalize(VALUE self)
{
    Image *image, *new_image;
#if defined(IMAGEMAGICK_7)
    ExceptionInfo *exception;
#endif

    image = rm_check_destroyed(self);
    new_image = rm_clone_image(image);

#if defined(IMAGEMAGICK_7)
    exception = AcquireExceptionInfo();
    GVL_STRUCT_TYPE(NormalizeImage) args = { new_image, exception };
    CALL_FUNC_WITHOUT_GVL(GVL_FUNC(NormalizeImage), &args);
    rm_check_exception(exception, new_image, DestroyOnError);
    DestroyExceptionInfo(exception);
#else
    GVL_STRUCT_TYPE(NormalizeImage) args = { new_image };
    CALL_FUNC_WITHOUT_GVL(GVL_FUNC(NormalizeImage), &args);
    rm_check_image_exception(new_image, DestroyOnError);
#endif

    return rm_image_new(new_image);
}


/**
 * Enhances the contrast of a color image by adjusting the pixel color to span the entire range of
 * colors available. Only the specified channels are normalized.
 *
 * @overload normalize_channel(channel = Magick::AllChannels)
 *   @param channel [Magick::ChannelType] a ChannelType arguments.
 *
 * @overload normalize_channel(*channels)
 *   @param *channels [Magick::ChannelType] one or more ChannelType arguments.
 *
 * @return [Magick::Image] a new image
 */
VALUE
Image_normalize_channel(int argc, VALUE *argv, VALUE self)
{
    Image *image, *new_image;
    ChannelType channels;
#if defined(IMAGEMAGICK_7)
    ExceptionInfo *exception;
#endif

    image = rm_check_destroyed(self);
    channels = extract_channels(&argc, argv);
    // Ensure all arguments consumed.
    if (argc > 0)
    {
        raise_ChannelType_error(argv[argc-1]);
    }

    new_image = rm_clone_image(image);

#if defined(IMAGEMAGICK_7)
    exception = AcquireExceptionInfo();
    BEGIN_CHANNEL_MASK(new_image, channels);
    GVL_STRUCT_TYPE(NormalizeImage) args = { new_image, exception };
    CALL_FUNC_WITHOUT_GVL(GVL_FUNC(NormalizeImage), &args);
    END_CHANNEL_MASK(new_image);
    rm_check_exception(exception, new_image, DestroyOnError);
    DestroyExceptionInfo(exception);
#else
    GVL_STRUCT_TYPE(NormalizeImageChannel) args = { new_image, channels };
    CALL_FUNC_WITHOUT_GVL(GVL_FUNC(NormalizeImageChannel), &args);
    rm_check_image_exception(new_image, DestroyOnError);
#endif

    return rm_image_new(new_image);
}


/**
 * Get the normalized mean error per pixel computed when an image is color reduced.
 *
 * @return [Float] the normalized mean error
 */
VALUE
Image_normalized_mean_error(VALUE self)
{
    IMPLEMENT_TYPED_ATTR_READERF(Image, normalized_mean_error, error.normalized_mean_error, dbl, &rm_image_data_type);
}

/**
 * Get The normalized maximum error per pixel computed when an image is color reduced.
 *
 * @return [Float] the normalized maximum error
 */
VALUE
Image_normalized_maximum_error(VALUE self)
{
    IMPLEMENT_TYPED_ATTR_READERF(Image, normalized_maximum_error, error.normalized_maximum_error, dbl, &rm_image_data_type);
}


/**
 * Return the number of unique colors in the image.
 *
 * @return [Integer] number of unique colors
 */
VALUE
Image_number_colors(VALUE self)
{
    Image *image;
    ExceptionInfo *exception;
    size_t n = 0;

    image = rm_check_destroyed(self);
    exception = AcquireExceptionInfo();

    GVL_STRUCT_TYPE(GetNumberColors) args = { image, NULL, exception };
    n = (size_t)CALL_FUNC_WITHOUT_GVL(GVL_FUNC(GetNumberColors), &args);
    CHECK_EXCEPTION();

    DestroyExceptionInfo(exception);

    return ULONG2NUM(n);
}


/**
 * Get the number of bytes to skip over when reading raw image.
 *
 * @return [Integer] the offset
 */
VALUE
Image_offset(VALUE self)
{
    IMPLEMENT_TYPED_ATTR_READER(Image, offset, long, &rm_image_data_type);
}

/**
 * Set the number of bytes to skip over when reading raw image.
 *
 * @param val [Numeric] the offset
 * @return [Numeric] the given offset
 */
VALUE
Image_offset_eq(VALUE self, VALUE val)
{
    IMPLEMENT_TYPED_ATTR_WRITER(Image, offset, long, &rm_image_data_type);
}


/**
 * Apply a special effect filter that simulates an oil painting.
 *
 * @overload oil_paint(radius = 3.0)
 *   @param radius [Numeric] The radius of the Gaussian in pixels.
 *   @return [Magick::Image] a new image
 */
VALUE
Image_oil_paint(int argc, VALUE *argv, VALUE self)
{
    Image *image, *new_image;
    double radius = 3.0;
    ExceptionInfo *exception;
#if defined(IMAGEMAGICK_7)
    double sigma = 1.0;
#endif

    image = rm_check_destroyed(self);
    switch (argc)
    {
        case 1:
            radius = NUM2DBL(argv[0]);
        case 0:
            break;
        default:
            rb_raise(rb_eArgError, "wrong number of arguments (%d for 0 or 1)", argc);
            break;
    }

    exception = AcquireExceptionInfo();

#if defined(IMAGEMAGICK_7)
    GVL_STRUCT_TYPE(OilPaintImage) args = { image, radius, sigma, exception };
#else
    GVL_STRUCT_TYPE(OilPaintImage) args = { image, radius, exception };
#endif
    new_image = (Image *)CALL_FUNC_WITHOUT_GVL(GVL_FUNC(OilPaintImage), &args);
    rm_check_exception(exception, new_image, DestroyOnError);
    DestroyExceptionInfo(exception);

    return rm_image_new(new_image);
}


/**
 * Change any pixel that matches target with the color defined by fill.
 *
 *  - By default a pixel must match the specified target color exactly.
 *  - Use {Image#fuzz=} to set the amount of tolerance acceptable to consider two colors as the
 *    same.
 *
 * @param target [Magick::Pixel, String] the color name
 * @param fill [Magick::Pixel, String] the color for filling
 * @return [Magick::Image] a new image
 * @see Image#fuzz=
 */
VALUE
Image_opaque(VALUE self, VALUE target, VALUE fill)
{
    Image *image, *new_image;
    MagickPixel target_pp;
    MagickPixel fill_pp;
    MagickBooleanType okay;
#if defined(IMAGEMAGICK_7)
    ExceptionInfo *exception;
#endif

    image = rm_check_destroyed(self);

    // Allow color name or Pixel
    Color_to_MagickPixel(image, &target_pp, target);
    Color_to_MagickPixel(image, &fill_pp, fill);

    new_image = rm_clone_image(image);

#if defined(IMAGEMAGICK_7)
    exception = AcquireExceptionInfo();
    GVL_STRUCT_TYPE(OpaquePaintImage) args = { new_image, &target_pp, &fill_pp, MagickFalse, exception };
    void *ret = CALL_FUNC_WITHOUT_GVL(GVL_FUNC(OpaquePaintImage), &args);
    okay = static_cast<MagickBooleanType>(reinterpret_cast<intptr_t &>(ret));
    rm_check_exception(exception, new_image, DestroyOnError);
    DestroyExceptionInfo(exception);
#else
    GVL_STRUCT_TYPE(OpaquePaintImageChannel) args = { new_image, DefaultChannels, &target_pp, &fill_pp, MagickFalse };
    void *ret = CALL_FUNC_WITHOUT_GVL(GVL_FUNC(OpaquePaintImageChannel), &args);
    okay = static_cast<MagickBooleanType>(reinterpret_cast<intptr_t &>(ret));
    rm_check_image_exception(new_image, DestroyOnError);
#endif

    if (!okay)
    {
        // Force exception
        DestroyImage(new_image);
        rm_ensure_result(NULL);
    }

    return rm_image_new(new_image);
}


/**
 * Changes all pixels having the target color to the fill color.
 * If invert is true, changes all the pixels that are not the target color to the fill color.
 *
 * @overload opaque_channel(target, fill, invert = false, fuzz = self.fuzz, channel = Magick::AllChannels)
 *   @param target [Magick::Pixel, String] the color name
 *   @param fill [Magick::Pixel, String] the color for filling
 *   @param invert [Boolean] If true, the target pixels are all the pixels that are not the target
 *     color. The default is the value of the target image's fuzz attribute
 *   @param fuzz [Numeric] Colors within this distance are considered equal to the target color.
 *   @param channel [Magick::ChannelType] a ChannelType arguments.
 *
 * @overload opaque_channel(target, fill, invert, fuzz, *channels)
 *   @param target [Magick::Pixel, String] the color name
 *   @param fill [Magick::Pixel, String] the color for filling
 *   @param invert [Boolean] If true, the target pixels are all the pixels that are not the target
 *     color. The default is the value of the target image's fuzz attribute
 *   @param fuzz [Numeric] Colors within this distance are considered equal to the target color.
 *   @param *channels [Magick::ChannelType] one or more ChannelType arguments.
 *
 * @return [Magick::Image] a new image
 */
VALUE
Image_opaque_channel(int argc, VALUE *argv, VALUE self)
{
    Image *image, *new_image;
    MagickPixel target_pp, fill_pp;
    ChannelType channels;
    double keep, fuzz;
    MagickBooleanType okay, invert = MagickFalse;
#if defined(IMAGEMAGICK_7)
    ExceptionInfo *exception;
#endif

    image = rm_check_destroyed(self);
    channels = extract_channels(&argc, argv);
    if (argc > 4)
    {
        raise_ChannelType_error(argv[argc-1]);
    }

    // Default fuzz value is image's fuzz attribute.
    fuzz = image->fuzz;

    switch (argc)
    {
        case 4:
            fuzz = NUM2DBL(argv[3]);
            if (fuzz < 0.0)
            {
                rb_raise(rb_eArgError, "fuzz must be >= 0.0 (%g given)", fuzz);
            }
        case 3:
            invert = (MagickBooleanType)RTEST(argv[2]);
        case 2:
            // Allow color name or Pixel
            Color_to_MagickPixel(image, &fill_pp, argv[1]);
            Color_to_MagickPixel(image, &target_pp, argv[0]);
            break;
        default:
            rb_raise(rb_eArgError, "wrong number of arguments (got %d, expected 2 or more)", argc);
            break;
    }

    new_image = rm_clone_image(image);
    keep = new_image->fuzz;
    new_image->fuzz = fuzz;

#if defined(IMAGEMAGICK_7)
    exception = AcquireExceptionInfo();
    BEGIN_CHANNEL_MASK(new_image, channels);
    GVL_STRUCT_TYPE(OpaquePaintImage) args = { new_image, &target_pp, &fill_pp, invert, exception };
    void *ret = CALL_FUNC_WITHOUT_GVL(GVL_FUNC(OpaquePaintImage), &args);
    okay = static_cast<MagickBooleanType>(reinterpret_cast<intptr_t &>(ret));
    END_CHANNEL_MASK(new_image);
    new_image->fuzz = keep;
    rm_check_exception(exception, new_image, DestroyOnError);
    DestroyExceptionInfo(exception);
#else
    GVL_STRUCT_TYPE(OpaquePaintImageChannel) args = { new_image, channels, &target_pp, &fill_pp, invert };
    void *ret = CALL_FUNC_WITHOUT_GVL(GVL_FUNC(OpaquePaintImageChannel), &args);
    okay = static_cast<MagickBooleanType>(reinterpret_cast<intptr_t &>(ret));

    new_image->fuzz = keep;
    rm_check_image_exception(new_image, DestroyOnError);
#endif

    if (!okay)
    {
        // Force exception
        DestroyImage(new_image);
        rm_ensure_result(NULL);
    }

    return rm_image_new(new_image);
}


/**
 * Returns true if all of the pixels in the receiver have an opacity value of OpaqueOpacity.
 *
 * @return [Boolean] true if opaque, false otherwise
 */
VALUE
Image_opaque_q(VALUE self)
{
#if defined(IMAGEMAGICK_7)
    return has_attribute(self, IsImageOpaque);
#else
    return has_attribute(self, IsOpaqueImage);
#endif
}


/**
 * Dithers the image to a predefined pattern. The threshold_map argument defines the pattern to use.
 *
 * - Default threshold_map is '2x2'
 * - Order of threshold_map must be 2, 3, or 4.
 *
 * @overload ordered_dither(threshold_map = '2x2')
 *   @param threshold_map [String, Numeric] the threshold
 *   @return [Magick::Image] a new image
 */
VALUE
Image_ordered_dither(int argc, VALUE *argv, VALUE self)
{
    Image *image, *new_image;
    int order;
    const char *threshold_map = "2x2";
    ExceptionInfo *exception;

    image = rm_check_destroyed(self);

    if (argc > 1)
    {
        rb_raise(rb_eArgError, "wrong number of arguments (%d for 0 or 1)", argc);
    }
    if (argc == 1)
    {
        if (TYPE(argv[0]) == T_STRING)
        {
            threshold_map = StringValueCStr(argv[0]);
        }
        else
        {
            order = NUM2INT(argv[0]);
            if (order == 3)
            {
                threshold_map = "3x3";
            }
            else if (order == 4)
            {
                threshold_map = "4x4";
            }
            else if (order != 2)
            {
                rb_raise(rb_eArgError, "order must be 2, 3, or 4 (%d given)", order);
            }
        }
    }

    new_image = rm_clone_image(image);

    exception = AcquireExceptionInfo();

#if defined(IMAGEMAGICK_7)
    GVL_STRUCT_TYPE(OrderedDitherImage) args = { new_image, threshold_map, exception };
    CALL_FUNC_WITHOUT_GVL(GVL_FUNC(OrderedDitherImage), &args);
#else
    GVL_STRUCT_TYPE(OrderedPosterizeImage) args = { new_image, threshold_map, exception };
    CALL_FUNC_WITHOUT_GVL(GVL_FUNC(OrderedPosterizeImage), &args);
#endif
    rm_check_exception(exception, new_image, DestroyOnError);

    DestroyExceptionInfo(exception);

    return rm_image_new(new_image);
}


/**
 * Get the value of the Exif Orientation Tag.
 *
 * @return [Magick::OrientationType] the orientation
 */
VALUE
Image_orientation(VALUE self)
{
    Image *image = rm_check_destroyed(self);
    return OrientationType_find(image->orientation);
}


/**
 * Set the orientation attribute.
 *
 * @param orientation [Magick::OrientationType] the orientation
 * @return [Magick::OrientationType] the given value
 */
VALUE
Image_orientation_eq(VALUE self, VALUE orientation)
{
    Image *image = rm_check_frozen(self);
    VALUE_TO_ENUM(orientation, image->orientation, OrientationType);
    return orientation;
}


/**
 * The page attribute getter.
 *
 * @return [Magick::Rectangle] the page rectangle
 */
VALUE
Image_page(VALUE self)
{
    Image *image = rm_check_destroyed(self);
    return Import_RectangleInfo(&image->page);
}


/**
 * The page attribute setter.
 *
 * @param rect [Magick::Rectangle] the page rectangle
 * @return [Magick::Rectangle] the given value
 */
VALUE
Image_page_eq(VALUE self, VALUE rect)
{
    Image *image = rm_check_frozen(self);
    Export_RectangleInfo(&image->page, rect);
    return rect;
}


/**
 * Changes the opacity value of all the pixels that match color to the value specified by opacity.
 * If invert is true, changes the pixels that don't match color.
 *
 * @overload paint_transparent(color, invert, fuzz, alpha: Magick::TransparentAlpha)
 *   @param color [Magick::Pixel, String] the color name
 *   @param invert [Boolean] If true, the target pixels are all the pixels that are not the target
 *     color.
 *   @param fuzz [Numeric] By default the pixel must match exactly, but you can specify a tolerance
 *     level by passing a positive value.
 *   @param alpha [Numeric] The new alpha value, either an alpha value or a number between 0 and
 *     QuantumRange. The default is TransparentAlpha.
 *   @return [Magick::Image] a new image
 */
VALUE
Image_paint_transparent(int argc, VALUE *argv, VALUE self)
{
    Image *image, *new_image;
    MagickPixel color;
    Quantum alpha = TransparentAlpha;
    double keep, fuzz;
    MagickBooleanType okay, invert;
#if defined(IMAGEMAGICK_7)
    ExceptionInfo *exception;
#endif
    VALUE color_obj, invert_obj, fuzz_obj, kwargs;

    image = rm_check_destroyed(self);

    rb_scan_args(argc, argv, "12:", &color_obj, &invert_obj, &fuzz_obj, &kwargs);

    Color_to_MagickPixel(image, &color, color_obj);
    invert = (invert_obj != Qnil) ? (MagickBooleanType)RTEST(invert_obj) : MagickFalse;
    fuzz = (fuzz_obj != Qnil) ? NUM2DBL(fuzz_obj) : image->fuzz;
    if (RTEST(kwargs))
    {
        alpha = get_named_alpha_value(kwargs);
    }

    new_image = rm_clone_image(image);

    // Use fuzz value from caller
    keep = new_image->fuzz;
    new_image->fuzz = fuzz;

#if defined(IMAGEMAGICK_7)
    exception = AcquireExceptionInfo();
    GVL_STRUCT_TYPE(TransparentPaintImage) args = { new_image, (const MagickPixel *)&color, alpha, invert, exception };
    void *ret = CALL_FUNC_WITHOUT_GVL(GVL_FUNC(TransparentPaintImage), &args);
    okay = static_cast<MagickBooleanType>(reinterpret_cast<intptr_t &>(ret));
    new_image->fuzz = keep;
    rm_check_exception(exception, new_image, DestroyOnError);
    DestroyExceptionInfo(exception);
#else
    GVL_STRUCT_TYPE(TransparentPaintImage) args = { new_image, (const MagickPixel *)&color, (Quantum)(QuantumRange - alpha), invert };
    void *ret = CALL_FUNC_WITHOUT_GVL(GVL_FUNC(TransparentPaintImage), &args);
    okay = static_cast<MagickBooleanType>(reinterpret_cast<intptr_t &>(ret));
    new_image->fuzz = keep;

    // Is it possible for TransparentPaintImage to silently fail?
    rm_check_image_exception(new_image, DestroyOnError);
#endif

    if (!okay)
    {
        // Force exception
        DestroyImage(new_image);
        rm_ensure_result(NULL);
    }

    return rm_image_new(new_image);
}


/**
 * Return true if the image is PseudoClass and has 256 unique colors or less.
 *
 * @return [Boolean] true if palette, otherwise false
 */
VALUE
Image_palette_q(VALUE self)
{
#if defined(IMAGEMAGICK_7)
    return has_image_attribute(self, IsPaletteImage);
#else
    return has_attribute(self, IsPaletteImage);
#endif
}


/**
 * Returns all the properties of an image or image sequence except for the pixels.
 *
 * @param file_arg [File, String] the file containing image data or file name
 * @yield [info]
 * @yieldparam info [Magick::Image::Info]
 * @return [Array<Magick::Image>] an array of 1 or more new image objects (without pixel data)
 * @see Image#read
 */
VALUE
Image_ping(VALUE klass, VALUE file_arg)
{
    return rd_image(klass, file_arg, GVL_FUNC(PingImage));
}


/**
 * Get/set the color of the pixel at x, y.
 *
 * @overload pixel_color(x, y)
 *   Get the color
 *   @param x [Numeric] The x-coordinates of the pixel.
 *   @param y [Numeric] The y-coordinates of the pixel.
 *   @return [Magick::Pixel] the pixel at x, y.
 *
 * @overload pixel_color(x, y, color)
 *   Set the color
 *   @param x [Numeric] The x-coordinates of the pixel.
 *   @param y [Numeric] The y-coordinates of the pixel.
 *   @param color [Magick::Pixel, String] the color
 *   @return [Magick::Pixel] the old color at x, y.
 */
VALUE
Image_pixel_color(int argc, VALUE *argv, VALUE self)
{
    Image *image;
    Pixel new_color;
    PixelPacket old_color;
    ExceptionInfo *exception;
    long x, y;
    unsigned int set = False;
    MagickBooleanType okay;
#if defined(IMAGEMAGICK_7)
    Quantum *pixel;
    const Quantum *old_pixel;
#else
    PixelPacket *pixel;
    const PixelPacket *old_pixel;
    MagickPixel mpp;
    IndexPacket *indexes;
#endif

    memset(&old_color, 0, sizeof(old_color));

    image = rm_check_destroyed(self);

    switch (argc)
    {
        case 3:
            rb_check_frozen(self);
            set = True;
            // Replace with new color? The arg can be either a color name or
            // a Magick::Pixel.
            Color_to_Pixel(&new_color, argv[2]);
        case 2:
            break;
        default:
            rb_raise(rb_eArgError, "wrong number of arguments (%d for 2 or 3)", argc);
            break;
    }

    x = NUM2LONG(argv[0]);
    y = NUM2LONG(argv[1]);

    // Get the color of a pixel
    if (!set)
    {
        exception = AcquireExceptionInfo();
        GVL_STRUCT_TYPE(GetVirtualPixels) args = { image, x, y, 1, 1, exception };
        void *ret = CALL_FUNC_WITHOUT_GVL(GVL_FUNC(GetVirtualPixels), &args);
        old_pixel = reinterpret_cast<decltype(old_pixel)>(ret);
        CHECK_EXCEPTION();

        DestroyExceptionInfo(exception);

#if defined(IMAGEMAGICK_7)
        old_color.red   = GetPixelRed(image, old_pixel) + 0.5;
        old_color.green = GetPixelGreen(image, old_pixel) + 0.5;
        old_color.blue  = GetPixelBlue(image, old_pixel) + 0.5;
        old_color.alpha = GetPixelAlpha(image, old_pixel) + 0.5;
        old_color.black = GetPixelBlack(image, old_pixel) + 0.5;
        return Pixel_from_PixelPacket(&old_color);
#else
        old_color = *old_pixel;
        indexes = GetAuthenticIndexQueue(image);
        // PseudoClass
        if (image->storage_class == PseudoClass)
        {
            old_color = image->colormap[(unsigned long)*indexes];
        }
        if (!image->matte)
        {
            old_color.opacity = OpaqueOpacity;
        }

        rm_init_magickpixel(image, &mpp);
        mpp.red = GetPixelRed(&old_color);
        mpp.green = GetPixelGreen(&old_color);
        mpp.blue = GetPixelBlue(&old_color);
        mpp.opacity = GetPixelOpacity(&old_color);
        if (indexes)
        {
            mpp.index = GetPixelIndex(indexes);
        }
        return Pixel_from_MagickPixel(&mpp);
#endif
    }

    // ImageMagick segfaults if the pixel location is out of bounds.
    // Do what IM does and return the background color.
    if (x < 0 || y < 0 || (unsigned long)x >= image->columns || (unsigned long)y >= image->rows)
    {
        return Pixel_from_PixelColor(&image->background_color);
    }

#if defined(IMAGEMAGICK_7)
    exception = AcquireExceptionInfo();
#endif

    if (image->storage_class == PseudoClass)
    {
#if defined(IMAGEMAGICK_7)
        GVL_STRUCT_TYPE(SetImageStorageClass) args = { image, DirectClass, exception };
        void *ret = CALL_FUNC_WITHOUT_GVL(GVL_FUNC(SetImageStorageClass), &args);
        okay = static_cast<MagickBooleanType>(reinterpret_cast<intptr_t &>(ret));
        CHECK_EXCEPTION();
        if (!okay)
        {
            DestroyExceptionInfo(exception);
            rb_raise(Class_ImageMagickError, "SetImageStorageClass failed. Can't set pixel color.");
        }
#else
        GVL_STRUCT_TYPE(SetImageStorageClass) args = { image, DirectClass };
        void *ret = CALL_FUNC_WITHOUT_GVL(GVL_FUNC(SetImageStorageClass), &args);
        okay = static_cast<MagickBooleanType>(reinterpret_cast<intptr_t &>(ret));
        rm_check_image_exception(image, RetainOnError);
        if (!okay)
        {
            rb_raise(Class_ImageMagickError, "SetImageStorageClass failed. Can't set pixel color.");
        }
#endif
    }

#if defined(IMAGEMAGICK_6)
    exception = AcquireExceptionInfo();
#endif

    GVL_STRUCT_TYPE(GetAuthenticPixels) args = { image, x, y, 1, 1, exception };
    void *ret = CALL_FUNC_WITHOUT_GVL(GVL_FUNC(GetAuthenticPixels), &args);
    pixel = reinterpret_cast<decltype(pixel)>(ret);
    CHECK_EXCEPTION();

    if (pixel)
    {
#if defined(IMAGEMAGICK_7)
        old_color.red   = GetPixelRed(image, pixel) + 0.5;
        old_color.green = GetPixelGreen(image, pixel) + 0.5;
        old_color.blue  = GetPixelBlue(image, pixel) + 0.5;
        old_color.alpha = GetPixelAlpha(image, pixel) + 0.5;
        old_color.black = GetPixelBlack(image, pixel) + 0.5;

        SetPixelRed(image,   new_color.red,   pixel);
        SetPixelGreen(image, new_color.green, pixel);
        SetPixelBlue(image,  new_color.blue,  pixel);
        SetPixelAlpha(image, new_color.alpha, pixel);
        SetPixelBlack(image, new_color.black, pixel);
#else
        old_color = *pixel;
        indexes = GetAuthenticIndexQueue(image);
        if (!image->matte)
        {
            old_color.opacity = OpaqueOpacity;
        }

        SetPixelRed(pixel,     new_color.red);
        SetPixelGreen(pixel,   new_color.green);
        SetPixelBlue(pixel,    new_color.blue);
        SetPixelOpacity(pixel, new_color.opacity);
        if (indexes)
        {
            SetPixelIndex(indexes, new_color.black);
        }
#endif

        GVL_STRUCT_TYPE(SyncAuthenticPixels) args = { image, exception };
        CALL_FUNC_WITHOUT_GVL(GVL_FUNC(SyncAuthenticPixels), &args);
        CHECK_EXCEPTION();
    }

    DestroyExceptionInfo(exception);

    return Pixel_from_PixelPacket(&old_color);
}


/**
 * Get the "interpolate" field.
 *
 * @return [Magick::PixelInterpolateMethod] the interpolate field
 * @see Image#pixel_interpolation_method=
 */
VALUE
Image_pixel_interpolation_method(VALUE self)
{
    Image *image = rm_check_destroyed(self);
    return PixelInterpolateMethod_find(image->interpolate);
}


/**
 * Set the "interpolate" field.
 *
 * @param method [Magick::PixelInterpolateMethod] the interpolate field
 * @return [Magick::PixelInterpolateMethod] the given method
 * @see Image#pixel_interpolation_method
 */
VALUE
Image_pixel_interpolation_method_eq(VALUE self, VALUE method)
{
    Image *image = rm_check_frozen(self);
    VALUE_TO_ENUM(method, image->interpolate, PixelInterpolateMethod);
    return method;
}


/**
 * Produce an image that looks like a Polaroid instant picture. If the image has a "Caption"
 * property, the value is used as a caption.
 *
 * The following annotate attributes control the label rendering:
 * align, decorate, density, encoding, fill, font, font_family, font_stretch, font_style,
 * font_weight, gravity, pointsize, stroke, stroke_width, text_antialias, undercolor.
 *
 * @overload polaroid(angle = -5.0)
 *   @param angle [Numeric] The resulting image is rotated by this amount, measured in degrees.
 *
 * @overload polaroid(angle = -5.0)
 *   If present a block, optional arguments may be specified in a block associated with the method.
 *   These arguments control the shadow color and how the label is rendered.
 *   By default the shadow color is gray75. To specify a different shadow color,
 *   use options.shadow_color.
 *   To specify a different border color (that is, the color of the image border) use options.border_color.
 *   Both of these methods accept either a color name or a Pixel argument.
 *   @param angle [Numeric] The resulting image is rotated by this amount, measured in degrees.
 *   @yield [opt]
 *   @yieldparam opt [Magick::Image::PolaroidOptions]
 *
 * @return [Magick::Image] a new image
 */
VALUE
Image_polaroid(int argc, VALUE *argv, VALUE self)
{
    Image *image, *clone, *new_image;
    VALUE options;
    double angle = -5.0;
    Draw *draw;
    ExceptionInfo *exception;
#if defined(IMAGEMAGICK_7)
    const char *caption;
#endif

    image = rm_check_destroyed(self);

    switch (argc)
    {
        case 1:
            angle = NUM2DBL(argv[0]);
        case 0:
            break;
        default:
            rb_raise(rb_eArgError, "wrong number of arguments (%d for 0 or 1)", argc);
            break;
    }

    options = rm_polaroid_new();
    TypedData_Get_Struct(options, Draw, &rm_draw_data_type, draw);

    clone = rm_clone_image(image);
    clone->background_color = draw->shadow_color;
    clone->border_color = draw->info->border_color;

    exception = AcquireExceptionInfo();
#if defined(IMAGEMAGICK_7)
    caption = GetImageProperty(clone, "Caption", exception);
    GVL_STRUCT_TYPE(PolaroidImage) args = { clone, draw->info, caption, angle, image->interpolate, exception };
    new_image = (Image *)CALL_FUNC_WITHOUT_GVL(GVL_FUNC(PolaroidImage), &args);
#else
    GVL_STRUCT_TYPE(PolaroidImage) args = { clone, draw->info, angle, exception };
    new_image = (Image *)CALL_FUNC_WITHOUT_GVL(GVL_FUNC(PolaroidImage), &args);
#endif
    rm_check_exception(exception, clone, DestroyOnError);

    DestroyImage(clone);
    DestroyExceptionInfo(exception);

    RB_GC_GUARD(options);

    return rm_image_new(new_image);
}


/**
 * Reduces the image to a limited number of colors for a "poster" effect.
 *
 * @overload posterize(levels = 4, dither = false)
 *   @param levels [Numeric] number of input arguments
 *   @param dither [Boolean] array of input arguments
 *   @return a new image
 */
VALUE
Image_posterize(int argc, VALUE *argv, VALUE self)
{
    Image *image, *new_image;
    MagickBooleanType dither = MagickFalse;
    unsigned long levels = 4;
#if defined(IMAGEMAGICK_7)
    DitherMethod dither_method;
    ExceptionInfo *exception;
#endif

    image = rm_check_destroyed(self);
    switch (argc)
    {
        case 2:
            dither = (MagickBooleanType) RTEST(argv[1]);
            /* fall through */
        case 1:
            levels = NUM2ULONG(argv[0]);
            /* fall through */
        case 0:
            break;
        default:
            rb_raise(rb_eArgError, "wrong number of arguments (%d for 0 to 2)", argc);
    }

    new_image = rm_clone_image(image);

#if defined(IMAGEMAGICK_7)
    exception = AcquireExceptionInfo();
    dither_method = dither ? RiemersmaDitherMethod : NoDitherMethod;
    GVL_STRUCT_TYPE(PosterizeImage) args = { new_image, levels, dither_method, exception };
    CALL_FUNC_WITHOUT_GVL(GVL_FUNC(PosterizeImage), &args);
    rm_check_exception(exception, new_image, DestroyOnError);
    DestroyExceptionInfo(exception);
#else
    GVL_STRUCT_TYPE(PosterizeImage) args = { new_image, levels, dither };
    CALL_FUNC_WITHOUT_GVL(GVL_FUNC(PosterizeImage), &args);
    rm_check_image_exception(new_image, DestroyOnError);
#endif

    return rm_image_new(new_image);
}


/**
 * Creates an image that contains 9 small versions of the receiver image. The center image is the
 * unchanged receiver. The other 8 images are variations created by transforming the receiver
 * according to the specified preview type with varying parameters.
 *
 * @return [Magick::Image] a new image
 */
VALUE
Image_preview(VALUE self, VALUE preview)
{
    Image *image, *new_image;
    PreviewType preview_type;
    ExceptionInfo *exception;

    image = rm_check_destroyed(self);
    VALUE_TO_ENUM(preview, preview_type, PreviewType);

    exception = AcquireExceptionInfo();
    GVL_STRUCT_TYPE(PreviewImage) args = { image, preview_type, exception };
    new_image = (Image *)CALL_FUNC_WITHOUT_GVL(GVL_FUNC(PreviewImage), &args);
    rm_check_exception(exception, new_image, DestroyOnError);

    DestroyExceptionInfo(exception);

    return rm_image_new(new_image);
}


/**
 * Set the image profile. If "profile" is nil, deletes the profile. Otherwise "profile" must be a
 * string containing the specified profile.
 *
 * @param name [String, nil] The profile name, or "*" to represent all the profiles in the image.
 * @param profile [String] The profile value, or nil to cause the profile to be removed.
 * @return [Magick::Image] self
 */
VALUE
Image_profile_bang(VALUE self, VALUE name, VALUE profile)
{

    if (profile == Qnil)
    {
        return Image_delete_profile(self, name);
    }
    else
    {
        return set_profile(self, StringValueCStr(name), profile);
    }

}


/**
 * Get image quality.
 *
 * @return [Integer] the quality
 */
VALUE
Image_quality(VALUE self)
{
    IMPLEMENT_TYPED_ATTR_READER(Image, quality, ulong, &rm_image_data_type);
}


/**
 * Return the image depth to the nearest Quantum (8, 16, or 32).
 *
 * @return [Integer] image depth
 */
VALUE
Image_quantum_depth(VALUE self)
{
    Image *image;
    unsigned long quantum_depth;

    image = rm_check_destroyed(self);
    quantum_depth = GetImageQuantumDepth(image, MagickFalse);

    return ULONG2NUM(quantum_depth);
}


/**
 * Performs the requested integer arithmetic operation on the selected channel of the image.
 * This method allows simple arithmetic operations on the component values of all pixels in an
 * image.
 * Of course, you could also do this in Ruby using get_pixels and store_pixels, or view, but
 * quantum_operator will be faster, especially for large numbers of pixels, since it does not need
 * to convert the pixels from C to Ruby.
 *
 * @overload quantum_operator(quantum_expression_op, rvalue, channel = Magick::AllChannels)
 *   @param quantum_expression_op [Magick::QuantumExpressionOperator] the operator
 *   @param rvalue [Numeric] the operation rvalue.
 *   @param channel [Magick::ChannelType] a ChannelType arguments.
 *
 * @overload quantum_operator(quantum_expression_op, rvalue, *channels)
 *   @param quantum_expression_op [Magick::QuantumExpressionOperator] the operator
 *   @param rvalue [Numeric] the operation rvalue.
 *   @param *channels [Magick::ChannelType] one or more ChannelType arguments.
 *
 * @return [Magick::Image] self
 */
VALUE
Image_quantum_operator(int argc, VALUE *argv, VALUE self)
{
    Image *image;
    QuantumExpressionOperator quantum_expression_op;
    MagickEvaluateOperator qop;
    double rvalue;
    ChannelType channel;
    ExceptionInfo *exception;

    image = rm_check_destroyed(self);

    // The default channel is AllChannels
    channel = AllChannels;

    /*
        If there are 3 arguments, argument 2 is a ChannelType argument.
        Arguments 1 and 0 are required and are the rvalue and operator,
        respectively.
    */
    switch (argc)
    {
        case 3:
            VALUE_TO_ENUM(argv[2], channel, ChannelType);
            /* Fall through */
        case 2:
            rvalue = NUM2DBL(argv[1]);
            VALUE_TO_ENUM(argv[0], quantum_expression_op, QuantumExpressionOperator);
            break;
        default:
            rb_raise(rb_eArgError, "wrong number of arguments (%d for 2 or 3)", argc);
            break;
    }

    // Map QuantumExpressionOperator to MagickEvaluateOperator
    switch (quantum_expression_op)
    {
        default:
        case UndefinedQuantumOperator:
            qop = UndefinedEvaluateOperator;
            break;
        case AddQuantumOperator:
            qop = AddEvaluateOperator;
            break;
        case AndQuantumOperator:
            qop = AndEvaluateOperator;
            break;
        case DivideQuantumOperator:
            qop = DivideEvaluateOperator;
            break;
        case LShiftQuantumOperator:
            qop = LeftShiftEvaluateOperator;
            break;
        case MaxQuantumOperator:
            qop = MaxEvaluateOperator;
            break;
        case MinQuantumOperator:
            qop = MinEvaluateOperator;
            break;
        case MultiplyQuantumOperator:
            qop = MultiplyEvaluateOperator;
            break;
        case OrQuantumOperator:
            qop = OrEvaluateOperator;
            break;
        case RShiftQuantumOperator:
            qop = RightShiftEvaluateOperator;
            break;
        case SubtractQuantumOperator:
            qop = SubtractEvaluateOperator;
            break;
        case XorQuantumOperator:
            qop = XorEvaluateOperator;
            break;
        case PowQuantumOperator:
            qop = PowEvaluateOperator;
            break;
        case LogQuantumOperator:
            qop = LogEvaluateOperator;
            break;
        case ThresholdQuantumOperator:
            qop = ThresholdEvaluateOperator;
            break;
        case ThresholdBlackQuantumOperator:
            qop = ThresholdBlackEvaluateOperator;
            break;
        case ThresholdWhiteQuantumOperator:
            qop = ThresholdWhiteEvaluateOperator;
            break;
        case GaussianNoiseQuantumOperator:
            qop = GaussianNoiseEvaluateOperator;
            break;
        case ImpulseNoiseQuantumOperator:
            qop = ImpulseNoiseEvaluateOperator;
            break;
        case LaplacianNoiseQuantumOperator:
            qop = LaplacianNoiseEvaluateOperator;
            break;
        case MultiplicativeNoiseQuantumOperator:
            qop = MultiplicativeNoiseEvaluateOperator;
            break;
        case PoissonNoiseQuantumOperator:
            qop = PoissonNoiseEvaluateOperator;
            break;
        case UniformNoiseQuantumOperator:
            qop = UniformNoiseEvaluateOperator;
            break;
        case CosineQuantumOperator:
            qop = CosineEvaluateOperator;
            break;
        case SetQuantumOperator:
            qop = SetEvaluateOperator;
            break;
        case SineQuantumOperator:
            qop = SineEvaluateOperator;
            break;
        case AddModulusQuantumOperator:
            qop = AddModulusEvaluateOperator;
            break;
        case MeanQuantumOperator:
            qop = MeanEvaluateOperator;
            break;
        case AbsQuantumOperator:
            qop = AbsEvaluateOperator;
            break;
        case ExponentialQuantumOperator:
            qop = ExponentialEvaluateOperator;
            break;
        case MedianQuantumOperator:
            qop = MedianEvaluateOperator;
            break;
        case SumQuantumOperator:
            qop = SumEvaluateOperator;
            break;
        case RootMeanSquareQuantumOperator:
            qop = RootMeanSquareEvaluateOperator;
            break;
    }

    exception = AcquireExceptionInfo();
#if defined(IMAGEMAGICK_7)
    BEGIN_CHANNEL_MASK(image, channel);
    GVL_STRUCT_TYPE(EvaluateImage) args = { image, qop, rvalue, exception };
    CALL_FUNC_WITHOUT_GVL(GVL_FUNC(EvaluateImage), &args);
    END_CHANNEL_MASK(image);
#else
    GVL_STRUCT_TYPE(EvaluateImageChannel) args = { image, channel, qop, rvalue, exception };
    CALL_FUNC_WITHOUT_GVL(GVL_FUNC(EvaluateImageChannel), &args);
#endif
    CHECK_EXCEPTION();

    DestroyExceptionInfo(exception);

    return self;
}


/**
 * Analyzes the colors within a reference image and chooses a fixed number of colors to represent
 * the image. The goal of the algorithm is to minimize the difference between the input and output
 * image while minimizing the processing time.
 *
 * @overload quantize(number_colors = 256, colorspace = Magick::RGBColorspace, dither = true, tree_depth = 0, measure_error = false)
 *   @param number_colors [Numeric] The maximum number of colors in the result image.
 *   @param colorspace [Magick::ColorspaceType] The colorspace to quantize in.
 *   @param dither [Boolean] If true, Magick::RiemersmaDitherMethod will be used as
 *     DitherMethod. otherwise NoDitherMethod.
 *   @param tree_depth [Numeric] The tree depth to use while quantizing. The values 0 and 1 support
 *     automatic tree depth determination. The tree depth may be forced via values ranging from 2 to
 *     8. The ideal tree depth depends on the characteristics of the input image, and may be
 *     determined through experimentation.
 *   @param measure_error [Boolean] Set to true to calculate quantization errors when quantizing the
 *     image.
 *   @return [Magick::Image] a new image
 */
VALUE
Image_quantize(int argc, VALUE *argv, VALUE self)
{
    Image *image, *new_image;
    QuantizeInfo quantize_info;
#if defined(IMAGEMAGICK_7)
    ExceptionInfo *exception;
#endif

    image = rm_check_destroyed(self);
    GetQuantizeInfo(&quantize_info);

    switch (argc)
    {
        case 5:
            quantize_info.measure_error = (MagickBooleanType) RTEST(argv[4]);
        case 4:
            quantize_info.tree_depth = NUM2UINT(argv[3]);
        case 3:
            if (rb_obj_is_kind_of(argv[2], Class_DitherMethod))
            {
                VALUE_TO_ENUM(argv[2], quantize_info.dither_method, DitherMethod);
#if defined(IMAGEMAGICK_6)
                quantize_info.dither = (MagickBooleanType)(quantize_info.dither_method != NoDitherMethod);
#endif
            }
            else
            {
#if defined(IMAGEMAGICK_7)
                quantize_info.dither_method = RTEST(argv[2]) ? RiemersmaDitherMethod : NoDitherMethod;
#else
                quantize_info.dither = (MagickBooleanType) RTEST(argv[2]);
#endif
            }
        case 2:
            VALUE_TO_ENUM(argv[1], quantize_info.colorspace, ColorspaceType);
        case 1:
            quantize_info.number_colors = NUM2UINT(argv[0]);
        case 0:
            break;
        default:
            rb_raise(rb_eArgError, "wrong number of arguments (%d for 0 to 5)", argc);
            break;
    }

    new_image = rm_clone_image(image);

#if defined(IMAGEMAGICK_7)
    exception = AcquireExceptionInfo();
    GVL_STRUCT_TYPE(QuantizeImage) args = { &quantize_info, new_image, exception };
    CALL_FUNC_WITHOUT_GVL(GVL_FUNC(QuantizeImage), &args);
    rm_check_exception(exception, new_image, DestroyOnError);
    DestroyExceptionInfo(exception);
#else
    GVL_STRUCT_TYPE(QuantizeImage) args = { &quantize_info, new_image };
    CALL_FUNC_WITHOUT_GVL(GVL_FUNC(QuantizeImage), &args);
    rm_check_image_exception(new_image, DestroyOnError);
#endif

    return rm_image_new(new_image);
}


/**
 * Applies a radial blur to the image.
 *
 * @param angle_obj [Numeric] the angle (in degrees)
 * @return [Magick::Image] a new image
 */
VALUE
Image_radial_blur(VALUE self, VALUE angle_obj)
{
    Image *image, *new_image;
    ExceptionInfo *exception;
    double angle = NUM2DBL(angle_obj);

    image = rm_check_destroyed(self);
    exception = AcquireExceptionInfo();

    GVL_STRUCT_TYPE(RotationalBlurImage) args = { image, angle, exception };
    void *ret = CALL_FUNC_WITHOUT_GVL(GVL_FUNC(RotationalBlurImage), &args);
    new_image = reinterpret_cast<decltype(new_image)>(ret);
    rm_check_exception(exception, new_image, DestroyOnError);
    DestroyExceptionInfo(exception);

    return rm_image_new(new_image);
}


/**
 * Applies a radial blur to the selected image channels.
 *
 * @overload radial_blur_channel(angle, channel = Magick::AllChannels)
 *   @param angle [Numeric] the angle (in degrees)
 *   @param channel [Magick::ChannelType] a ChannelType arguments.
 *
 * @overload radial_blur_channel(angle, *channels)
 *   @param angle [Numeric] the angle (in degrees)
 *   @param *channels [Magick::ChannelType] one or more ChannelType arguments.
 *
 * @return [Magick::Image] a new image
 */
VALUE
Image_radial_blur_channel(int argc, VALUE *argv, VALUE self)
{
    Image *image, *new_image;
    ExceptionInfo *exception;
    ChannelType channels;
    double angle;

    image = rm_check_destroyed(self);
    channels = extract_channels(&argc, argv);

    // There must be 1 remaining argument.
    if (argc == 0)
    {
        rb_raise(rb_eArgError, "wrong number of arguments (0 for 1 or more)");
    }
    else if (argc > 1)
    {
        raise_ChannelType_error(argv[argc-1]);
    }

    angle = NUM2DBL(argv[0]);
    exception = AcquireExceptionInfo();

#if defined(IMAGEMAGICK_7)
    BEGIN_CHANNEL_MASK(image, channels);
    GVL_STRUCT_TYPE(RotationalBlurImage) args = { image, angle, exception };
    new_image = (Image *)CALL_FUNC_WITHOUT_GVL(GVL_FUNC(RotationalBlurImage), &args);
    CHANGE_RESULT_CHANNEL_MASK(new_image);
    END_CHANNEL_MASK(image);
#else
    GVL_STRUCT_TYPE(RotationalBlurImageChannel) args = { image, channels, angle, exception };
    new_image = (Image *)CALL_FUNC_WITHOUT_GVL(GVL_FUNC(RotationalBlurImageChannel), &args);
#endif
    rm_check_exception(exception, new_image, DestroyOnError);
    DestroyExceptionInfo(exception);

    return rm_image_new(new_image);
}


/**
 * Changes the value of individual pixels based on the intensity of each pixel compared to a random
 * threshold. The result is a low-contrast, two color image.
 *
 * @overload random_threshold_channel(geometry_str, channel = Magick::AllChannels)
 *   @param geometry_str [Magick::Geometry, String] A geometry string containing LOWxHIGH thresholds.
 *   @param channel [Magick::ChannelType] a ChannelType arguments.
 *
 * @overload random_threshold_channel(geometry_str, *channels)
 *   @param geometry_str [Magick::Geometry, String] A geometry string containing LOWxHIGH thresholds.
 *   @param *channels [Magick::ChannelType] one or more ChannelType arguments.
 *
 * @return [Magick::Image] a new image
 * @see https://www.imagemagick.org/Magick++/Geometry.html
 */
VALUE
Image_random_threshold_channel(int argc, VALUE *argv, VALUE self)
{
    Image *image, *new_image;
    ChannelType channels;
    char *thresholds;
    VALUE geom_str;
    ExceptionInfo *exception;

    image = rm_check_destroyed(self);

    channels = extract_channels(&argc, argv);

    // There must be 1 remaining argument.
    if (argc == 0)
    {
        rb_raise(rb_eArgError, "missing threshold argument");
    }
    else if (argc > 1)
    {
        raise_ChannelType_error(argv[argc-1]);
    }

    // Accept any argument that has a to_s method.
    geom_str = rb_String(argv[0]);
    thresholds = StringValueCStr(geom_str);

    new_image = rm_clone_image(image);

    exception = AcquireExceptionInfo();

#if defined(IMAGEMAGICK_7)
    {
        GeometryInfo geometry_info;

        if (ParseGeometry(thresholds, &geometry_info) == NoValue && *thresholds)
        {
            (void) ThrowMagickException(exception, GetMagickModule(), OptionError,
                                        "InvalidArgument", "'%s'", thresholds);
        }
        else
        {
            BEGIN_CHANNEL_MASK(new_image, channels);
            {
                GVL_STRUCT_TYPE(RandomThresholdImage) args = { new_image, geometry_info.rho, geometry_info.sigma, exception };
                CALL_FUNC_WITHOUT_GVL(GVL_FUNC(RandomThresholdImage), &args);
            }
            END_CHANNEL_MASK(new_image);
        }
    }
#else
    GVL_STRUCT_TYPE(RandomThresholdImageChannel) args = { new_image, channels, thresholds, exception };
    CALL_FUNC_WITHOUT_GVL(GVL_FUNC(RandomThresholdImageChannel), &args);
#endif
    rm_check_exception(exception, new_image, DestroyOnError);

    DestroyExceptionInfo(exception);

    RB_GC_GUARD(geom_str);

    return rm_image_new(new_image);
}


/**
 * Create a simulated three-dimensional button-like effect by lightening and darkening the edges of
 * the image. The "width" and "height" arguments define the width of the vertical and horizontal
 * edge of the effect. If "raised" is true, creates a raised effect, otherwise a lowered effect.
 *
 * @overload raise(width = 6, height = 6, raised = true)
 *   @param width [Numeric] The width of the raised edge in pixels.
 *   @param height [Numeric] The height of the raised edge in pixels.
 *   @param raised [Boolean] If true, the image is raised, otherwise lowered.
 *   @return [Magick::Image] a new image
 */
VALUE
Image_raise(int argc, VALUE *argv, VALUE self)
{
    Image *image, *new_image;
    RectangleInfo rect;
    MagickBooleanType raised = MagickTrue;      // default
#if defined(IMAGEMAGICK_7)
    ExceptionInfo *exception;
#endif

    memset(&rect, 0, sizeof(rect));
    rect.width = 6;         // default
    rect.height = 6;        // default

    image = rm_check_destroyed(self);
    switch (argc)
    {
        case 3:
            raised = (MagickBooleanType)RTEST(argv[2]);
        case 2:
            rect.height = NUM2ULONG(argv[1]);
        case 1:
            rect.width = NUM2ULONG(argv[0]);
        case 0:
            break;
        default:
            rb_raise(rb_eArgError, "wrong number of arguments (%d for 0 to 3)", argc);
            break;
    }

    new_image = rm_clone_image(image);

#if defined(IMAGEMAGICK_7)
    exception = AcquireExceptionInfo();
    GVL_STRUCT_TYPE(RaiseImage) args = { new_image, &rect, raised, exception };
    CALL_FUNC_WITHOUT_GVL(GVL_FUNC(RaiseImage), &args);
    rm_check_exception(exception, new_image, DestroyOnError);
    DestroyExceptionInfo(exception);
#else
    GVL_STRUCT_TYPE(RaiseImage) args = { new_image, &rect, raised };
    CALL_FUNC_WITHOUT_GVL(GVL_FUNC(RaiseImage), &args);
    rm_check_image_exception(new_image, DestroyOnError);
#endif

    return rm_image_new(new_image);
}


/**
 * Call ReadImage.
 *
 * @param file_arg [File, String] the file containing image data or file name
 * @yield [info]
 * @yieldparam info [Magick::Image::Info]
 * @return [Array<Magick::Image>] an array of 1 or more new image objects
 */
VALUE
Image_read(VALUE klass, VALUE file_arg)
{
    return rd_image(klass, file_arg, GVL_FUNC(ReadImage));
}


/**
 * Called when `rm_obj_to_s' raised an exception.
 *
 * No Ruby usage (internal function)
 *
 * @param arg the bad arg given
 * @return 0
 */
static VALUE
file_arg_rescue(VALUE arg, VALUE raised_exc ATTRIBUTE_UNUSED)
{
    rb_raise(rb_eTypeError, "argument must be path name or open file (%s given)",
             rb_class2name(CLASS_OF(arg)));
}


// aliases for common use of structure types; PingImage, ReadImage
typedef GVL_STRUCT_TYPE(PingImage) GVL_STRUCT_TYPE(rd_image);

/**
 * Transform arguments, call either ReadImage or PingImage.
 *
 * No Ruby usage (internal function)
 *
 * Notes:
 *   - Yields to a block to get Image::Info attributes before calling
 *     Read/PingImage
 *
 * @param klass the Ruby class for an Image
 * @param file the file containing image data
 * @param reader which image reader to use (ReadImage or PingImage)
 * @return an array of 1 or more new image objects
 * @see Image_read
 * @see Image_ping
 * @see array_from_images
 */

#if defined(__APPLE__) || defined(__FreeBSD__)
void sig_handler(int sig ATTRIBUTE_UNUSED)
{
}
#endif

static VALUE
rd_image(VALUE klass ATTRIBUTE_UNUSED, VALUE file, gvl_function_t fp)
{
    char *filename;
    size_t filename_l;
    Info *info;
    VALUE info_obj;
    Image *images;
    ExceptionInfo *exception;

    // Create a new Info structure for this read/ping
    info_obj = rm_info_new();
    TypedData_Get_Struct(info_obj, Info, &rm_info_data_type, info);

    if (TYPE(file) == T_FILE)
    {
        rb_io_t *fptr;

        // Ensure file is open - raise error if not
        GetOpenFile(file, fptr);
        rb_io_check_readable(fptr);
        SetImageInfoFile(info, rb_io_stdio_file(fptr));
    }
    else
    {
        // Convert arg to string. If an exception occurs raise an error condition.
        file = rb_rescue(RESCUE_FUNC(rb_String), file, RESCUE_EXCEPTION_HANDLER_FUNC(file_arg_rescue), file);

        filename = rm_str2cstr(file, &filename_l);
        filename_l = min(filename_l, MaxTextExtent-1);
        if (filename_l == 0)
        {
            rb_raise(rb_eArgError, "invalid path");
        }

        memcpy(info->filename, filename, (size_t)filename_l);
        info->filename[filename_l] = '\0';
        SetImageInfoFile(info, NULL);
    }

    exception = AcquireExceptionInfo();

#if defined(__APPLE__) || defined(__FreeBSD__)
    struct sigaction act, oldact;
    act.sa_handler = sig_handler;
    act.sa_flags = SA_RESTART;
    if (sigaction(SIGCHLD, &act, &oldact) < 0)
    {
        rb_sys_fail("sigaction");
    }
#endif

    GVL_STRUCT_TYPE(rd_image) args = { info, exception };
    images = (Image *)CALL_FUNC_WITHOUT_GVL(fp, &args);

#if defined(__APPLE__) || defined(__FreeBSD__)
    if (sigaction(SIGCHLD, &oldact, NULL) < 0)
    {
        rb_sys_fail("sigaction");
    }
#endif

    rm_check_exception(exception, images, DestroyOnError);
    DestroyExceptionInfo(exception);

    rm_ensure_result(images);

    rm_set_user_artifact(images, info);
    rm_sync_image_options(images, info);

    RB_GC_GUARD(info_obj);

    return array_from_images(images);
}


/**
 * Use this method to translate, scale, shear, or rotate image colors. Although you can use variable
 * sized matrices, typically you use a 5x5 for an RGBA image and a 6x6 for CMYKA. Populate the last
 * row with normalized values to translate.
 *
 * @param color_matrix [Array<Numeric>] An array of Float values representing the recolor matrix.
 * @return [Magick::Image] a new image
 */
VALUE
Image_recolor(VALUE self, VALUE color_matrix)
{
    Image *image, *new_image;
    unsigned long order;
    long x, len;
    double *matrix;
    ExceptionInfo *exception;
    KernelInfo *kernel_info;

    image = rm_check_destroyed(self);
    color_matrix = rm_check_ary_type(color_matrix);

    // Allocate color matrix from Ruby's memory
    len = RARRAY_LEN(color_matrix);
    matrix = ALLOC_N(double, len);

    for (x = 0; x < len; x++)
    {
        VALUE element = rb_ary_entry(color_matrix, x);
        if (rm_check_num2dbl(element))
        {
            matrix[x] = NUM2DBL(element);
        }
        else
        {
            xfree(matrix);
            rb_raise(rb_eTypeError, "type mismatch: %s given", rb_class2name(CLASS_OF(element)));
        }
    }

    order = (unsigned long)sqrt((double)(len + 1.0));

    exception = AcquireExceptionInfo();
#if defined(IMAGEMAGICK_7)
    kernel_info = AcquireKernelInfo(NULL, exception);
    if (rm_should_raise_exception(exception, RetainExceptionRetention))
    {
        if (kernel_info != (KernelInfo *) NULL)
        {
            DestroyKernelInfo(kernel_info);
        }
        xfree((void *)matrix);
        rm_raise_exception(exception);
    }
#else
    kernel_info = AcquireKernelInfo(NULL);
#endif
    if (kernel_info == (KernelInfo *) NULL)
    {
        xfree((void *) matrix);
        DestroyExceptionInfo(exception);
        return Qnil;
    }
    kernel_info->width = order;
    kernel_info->height = order;
    kernel_info->values = (double *) matrix;

    GVL_STRUCT_TYPE(ColorMatrixImage) args = { image, kernel_info, exception };
    new_image = (Image *)CALL_FUNC_WITHOUT_GVL(GVL_FUNC(ColorMatrixImage), &args);
    kernel_info->values = (double *) NULL;
    DestroyKernelInfo(kernel_info);
    xfree((void *) matrix);

    rm_check_exception(exception, new_image, DestroyOnError);
    DestroyExceptionInfo(exception);

    return rm_image_new(new_image);
}


/**
 * Read a Base64-encoded image.
 *
 * @param content [String] the content
 * @yield [info]
 * @yieldparam info [Magick::Image::Info]
 * @return [Array<Magick::Image>] an array of new images
 */
VALUE
Image_read_inline(VALUE self ATTRIBUTE_UNUSED, VALUE content)
{
    VALUE info_obj;
    Image *images;
    ImageInfo *info;
    char *image_data;
    size_t x, image_data_l;
    unsigned char *blob;
    size_t blob_l;
    ExceptionInfo *exception;

    image_data = rm_str2cstr(content, &image_data_l);

    // Search for a comma. If found, we'll set the start of the
    // image data just following the comma. Otherwise we'll assume
    // the image data starts with the first byte.
    for (x = 0; x < image_data_l; x++)
    {
        if (image_data[x] == ',')
        {
            break;
        }
    }
    if (x < image_data_l)
    {
        image_data += x + 1;
    }

    GVL_STRUCT_TYPE(Base64Decode) args_Base64Decode = { image_data, &blob_l };
    blob = (unsigned char *)CALL_FUNC_WITHOUT_GVL(GVL_FUNC(Base64Decode), &args_Base64Decode);
    if (blob_l == 0)
    {
        rb_raise(rb_eArgError, "can't decode image");
    }

    exception = AcquireExceptionInfo();

    // Create a new Info structure for this read. About the
    // only useful attribute that can be set is `format'.
    info_obj = rm_info_new();
    TypedData_Get_Struct(info_obj, Info, &rm_info_data_type, info);

    GVL_STRUCT_TYPE(BlobToImage) args_BlobToImage = { info, blob, blob_l, exception };
    images = (Image *)CALL_FUNC_WITHOUT_GVL(GVL_FUNC(BlobToImage), &args_BlobToImage);
    magick_free((void *)blob);

    rm_check_exception(exception, images, DestroyOnError);
    DestroyExceptionInfo(exception);

    rm_set_user_artifact(images, info);
    rm_sync_image_options(images, info);

    RB_GC_GUARD(info_obj);

    return array_from_images(images);
}


/**
 * Convert a list of images to an array of Image objects.
 *
 * No Ruby usage (internal function)
 *
 * @param images the images
 * @return array of images
 */
static VALUE
array_from_images(Image *images)
{
    VALUE image_obj, image_ary;
    Image *image;

    // Orphan the image, create an Image object, add it to the array.

    image_ary = rb_ary_new();
    while (images)
    {
        image = RemoveFirstImageFromList(&images);
        image_obj = rm_image_new(image);
        rb_ary_push(image_ary, image_obj);
    }

    RB_GC_GUARD(image_obj);
    RB_GC_GUARD(image_ary);

    return image_ary;
}


/**
 * Smooth the contours of an image while still preserving edge information.
 *
 * @param radius [Numeric] A neighbor is defined by radius. Use a radius of 0 and reduce_noise
 *   selects a suitable radius for you.
 * @return [Magick::Image] a new image
 */
VALUE
Image_reduce_noise(VALUE self, VALUE radius)
{
    Image *image, *new_image;
    ExceptionInfo *exception;
    size_t radius_size = NUM2SIZET(radius);

    image = rm_check_destroyed(self);

    exception = AcquireExceptionInfo();
    GVL_STRUCT_TYPE(StatisticImage) args = { image, NonpeakStatistic, radius_size, radius_size, exception };
    new_image = (Image *)CALL_FUNC_WITHOUT_GVL(GVL_FUNC(StatisticImage), &args);
    rm_check_exception(exception, new_image, DestroyOnError);

    DestroyExceptionInfo(exception);

    return rm_image_new(new_image);
}


/**
 * Reduce the number of colors in img to the colors used by remap_image. If a dither method is
 * specified then the given colors are dithered over the image as necessary, otherwise the closest
 * color (in RGB colorspace) is selected to replace that pixel in the image.
 *
 * @overload remap(remap_image, dither_method = Magick::RiemersmaDitherMethod)
 *   @param remap_image [Magick::Image, Magick::ImageList] The reference image or imagelist. If an
 *     imagelist, uses the current image.
 *   @param dither_method [Magick::DitherMethod] this object
 *   @return self
 */
VALUE
Image_remap(int argc, VALUE *argv, VALUE self)
{
    Image *image, *remap_image;
    QuantizeInfo quantize_info;
#if defined(IMAGEMAGICK_7)
    ExceptionInfo *exception;
#endif

    image = rm_check_frozen(self);

    GetQuantizeInfo(&quantize_info);

    switch (argc)
    {
        case 2:
            VALUE_TO_ENUM(argv[1], quantize_info.dither_method, DitherMethod);
#if defined(IMAGEMAGICK_6)
            quantize_info.dither = MagickTrue;
#endif
            break;
        case 1:
            break;
        default:
            rb_raise(rb_eArgError, "wrong number of arguments (%d for 1 or 2)", argc);
            break;
    }

    remap_image = rm_check_destroyed(rm_cur_image(argv[0]));

#if defined(IMAGEMAGICK_7)
    exception = AcquireExceptionInfo();
    GVL_STRUCT_TYPE(RemapImage) args = { &quantize_info, image, remap_image, exception };
    CALL_FUNC_WITHOUT_GVL(GVL_FUNC(RemapImage), &args);
    CHECK_EXCEPTION();
    DestroyExceptionInfo(exception);
#else
    GVL_STRUCT_TYPE(RemapImage) args = { &quantize_info, image, remap_image };
    CALL_FUNC_WITHOUT_GVL(GVL_FUNC(RemapImage), &args);
    rm_check_image_exception(image, RetainOnError);
#endif

    return self;
}


/**
 * Get the type of rendering intent.
 *
 * @return [Magick::RenderingIntent] the rendering intent
 */
VALUE
Image_rendering_intent(VALUE self)
{
    Image *image = rm_check_destroyed(self);
    return RenderingIntent_find(image->rendering_intent);
}


/**
 * Set the type of rendering intent..
 *
 * @param ri [Magick::RenderingIntent] the rendering intent
 * @return [Magick::RenderingIntent] the given value
 */
VALUE
Image_rendering_intent_eq(VALUE self, VALUE ri)
{
    Image *image = rm_check_frozen(self);
    VALUE_TO_ENUM(ri, image->rendering_intent, RenderingIntent);
    return ri;
}


#if defined(IMAGEMAGICK_7)
/**
 * Create new blurred image.
 *
 * No Ruby usage (internal function)
 *
 * @param image the image
 * @param blur the blur
 * @return NULL if not apply blur, otherwise a new image
 */
static Image*
blurred_image(Image* image, double blur)
{
    ExceptionInfo *exception;
    Image *new_image;

    exception = AcquireExceptionInfo();
    if (blur > 1.0)
    {
        GVL_STRUCT_TYPE(BlurImage) args = { image, blur, blur, exception };
        new_image = (Image *)CALL_FUNC_WITHOUT_GVL(GVL_FUNC(BlurImage), &args);
    }
    else
    {
        GVL_STRUCT_TYPE(SharpenImage) args = { image, blur, blur, exception };
        new_image = (Image *)CALL_FUNC_WITHOUT_GVL(GVL_FUNC(SharpenImage), &args);
    }
    rm_check_exception(exception, new_image, DestroyOnError);
    DestroyExceptionInfo(exception);

    return new_image;
}
#endif


/**
 * Resample image to specified horizontal resolution, vertical resolution,
 * filter and blur factor.
 *
 * No Ruby usage (internal function)
 *
 * @param bang whether the bang (!) version of the method was called
 * @param argc number of input arguments
 * @param argv array of input arguments
 * @param self this object
 * @return self if bang, otherwise a new image
 * @see Image_resample
 * @see Image_resample_bang
 */
static VALUE
resample(int bang, int argc, VALUE *argv, VALUE self)
{
    Image *image, *new_image;
    FilterType filter;
    double x_resolution, y_resolution, blur;
    double width, height;
    ExceptionInfo *exception;

    TypedData_Get_Struct(self, Image, &rm_image_data_type, image);

    // Set up defaults
    filter  = image->filter;
#if defined(IMAGEMAGICK_7)
    blur    = 1.0;
#else
    blur    = image->blur;
#endif
    x_resolution = 72.0;
    y_resolution = 72.0;

    switch (argc)
    {
        case 4:
            blur = NUM2DBL(argv[3]);
        case 3:
            VALUE_TO_ENUM(argv[2], filter, FilterType);
        case 2:
            y_resolution = NUM2DBL(argv[1]);
            if (y_resolution < 0.0)
            {
                rb_raise(rb_eArgError, "invalid y_resolution value (%lf given)", y_resolution);
            }
        case 1:
            x_resolution = NUM2DBL(argv[0]);
            if (x_resolution < 0.0)
            {
                rb_raise(rb_eArgError, "invalid x_resolution value (%lf given)", x_resolution);
            }
            if (argc == 1)
            {
                y_resolution = x_resolution;
            }
#if defined(IMAGEMAGICK_7)
            width = (x_resolution * image->columns /
                        (image->resolution.x == 0.0 ? 72.0 : image->resolution.x) + 0.5);
            height = (y_resolution * image->rows /
                         (image->resolution.y == 0.0 ? 72.0 : image->resolution.y) + 0.5);
#else
            width = (x_resolution * image->columns /
                        (image->x_resolution == 0.0 ? 72.0 : image->x_resolution) + 0.5);
            height = (y_resolution * image->rows /
                         (image->y_resolution == 0.0 ? 72.0 : image->y_resolution) + 0.5);
#endif
            if (width > (double)ULONG_MAX || height > (double)ULONG_MAX)
            {
                rb_raise(rb_eRangeError, "resampled image too big");
            }
            break;
        case 0:
            break;
        default:
            rb_raise(rb_eArgError, "wrong number of arguments (%d for 0 to 4)", argc);
            break;
    }

    exception = AcquireExceptionInfo();
#if defined(IMAGEMAGICK_7)
    Image *preprocess = blurred_image(image, blur);
    GVL_STRUCT_TYPE(ResampleImage) args = { preprocess, x_resolution, y_resolution, filter, exception };
    new_image = (Image *)CALL_FUNC_WITHOUT_GVL(GVL_FUNC(ResampleImage), &args);
    DestroyImage(preprocess);
#else
    GVL_STRUCT_TYPE(ResampleImage) args = { image, x_resolution, y_resolution, filter, blur, exception };
    new_image = (Image *)CALL_FUNC_WITHOUT_GVL(GVL_FUNC(ResampleImage), &args);
#endif
    rm_check_exception(exception, new_image, DestroyOnError);

    DestroyExceptionInfo(exception);

    if (bang)
    {
        rm_ensure_result(new_image);
        UPDATE_DATA_PTR(self, new_image);
        rm_image_destroy(image);
        return self;
    }
    return rm_image_new(new_image);
}

/**
 * Resample image to specified horizontal resolution, vertical resolution, filter and blur factor.
 *
 * Resize the image so that its rendered size remains the same as the original at the specified
 * target resolution. For example, if a 300 DPI image renders at 3 inches by 2 inches on a 300 DPI
 * device, when the image has been resampled to 72 DPI, it will render at 3 inches by 2 inches on a
 * 72 DPI device. Note that only a small number of image formats (e.g. JPEG, PNG, and TIFF) are
 * capable of storing the image resolution. For formats which do not support an image resolution,
 * the original resolution of the image must be specified via the density attribute prior to
 * specifying the resample resolution.
 *
 * @overload resample(x_resolution = 72.0, y_resolution = 72.0, filter = self.filter, blur = self.blur)
 *   @param x_resolution [Numeric] the target horizontal resolution.
 *   @param y_resolution [Numeric] the target vertical resolution.
 *   @param filter [Magick::FilterType] the filter type
 *   @param blur [Numeric] the blur size
 *   @return [Magick] a new image
 *   @see Image#resample!
 */
VALUE
Image_resample(int argc, VALUE *argv, VALUE self)
{
    rm_check_destroyed(self);
    return resample(False, argc, argv, self);
}


/**
 * Resample image to specified horizontal resolution, vertical resolution, filter and blur factor.
 * In-place form of {Image#resample}.
 *
 * @overload resample!(x_resolution = 72.0, y_resolution = 72.0, filter = self.filter, blur = self.blur)
 *   @param x_resolution [Numeric] the target horizontal resolution.
 *   @param y_resolution [Numeric] the target vertical resolution.
 *   @param filter [Magick::FilterType] the filter type
 *   @param blur [Numeric] the blur size
 *   @return [Magick] a new image
 *   @see Image#resample
 */
VALUE
Image_resample_bang(int argc, VALUE *argv, VALUE self)
{
    rm_check_frozen(self);
    return resample(True, argc, argv, self);
}


/**
 * Scale an image to the desired dimensions using the specified filter and blur
 * factor.
 *
 * No Ruby usage (internal function)
 *
 * @param bang whether the bang (!) version of the method was called
 * @param argc number of input arguments
 * @param argv array of input arguments
 * @param self this object
 * @return self if bang, otherwise a new image
 * @see Image_resize
 * @see Image_resize_bang
 */
static VALUE
resize(int bang, int argc, VALUE *argv, VALUE self)
{
    Image *image, *new_image;
    double scale_arg;
    FilterType filter;
    unsigned long rows, columns;
    double blur, drows, dcols;
    ExceptionInfo *exception;

    TypedData_Get_Struct(self, Image, &rm_image_data_type, image);

    // Set up defaults
    filter  = image->filter;
#if defined(IMAGEMAGICK_7)
    blur    = 1.0;
#else
    blur    = image->blur;
#endif
    rows    = image->rows;
    columns = image->columns;

    switch (argc)
    {
        case 4:
            blur = NUM2DBL(argv[3]);
        case 3:
            VALUE_TO_ENUM(argv[2], filter, FilterType);
        case 2:
            rows = NUM2ULONG(argv[1]);
            columns = NUM2ULONG(argv[0]);
            if (columns == 0 || rows == 0)
            {
                rb_raise(rb_eArgError, "invalid result dimension (%lu, %lu given)", columns, rows);
            }
            break;
        case 1:
            scale_arg = NUM2DBL(argv[0]);
            if (scale_arg < 0.0)
            {
                rb_raise(rb_eArgError, "invalid scale_arg value (%g given)", scale_arg);
            }
            drows = scale_arg * image->rows + 0.5;
            dcols = scale_arg * image->columns + 0.5;
            if (drows > (double)ULONG_MAX || dcols > (double)ULONG_MAX)
            {
                rb_raise(rb_eRangeError, "resized image too big");
            }
            rows = (unsigned long) drows;
            columns = (unsigned long) dcols;
            break;
        default:
            rb_raise(rb_eArgError, "wrong number of arguments (%d for 1 to 4)", argc);
            break;
    }

    exception = AcquireExceptionInfo();
#if defined(IMAGEMAGICK_7)
    Image *preprocess = (argc == 4) ? blurred_image(image, blur) : image;
    GVL_STRUCT_TYPE(ResizeImage) args = { preprocess, columns, rows, filter, exception };
    new_image = (Image *)CALL_FUNC_WITHOUT_GVL(GVL_FUNC(ResizeImage), &args);
    if (argc == 4)
    {
        DestroyImage(preprocess);
    }
#else
    GVL_STRUCT_TYPE(ResizeImage) args = { image, columns, rows, filter, blur, exception };
    new_image = (Image *)CALL_FUNC_WITHOUT_GVL(GVL_FUNC(ResizeImage), &args);
#endif
    rm_check_exception(exception, new_image, DestroyOnError);

    DestroyExceptionInfo(exception);

    if (bang)
    {
        rm_ensure_result(new_image);
        UPDATE_DATA_PTR(self, new_image);
        rm_image_destroy(image);
        return self;
    }
    return rm_image_new(new_image);
}


/**
 * Scale an image to the desired dimensions using the specified filter and blur factor.
 *
 * @overload resize(scale)
 *   @param scale [Numeric] You can use this argument instead of specifying the desired width and
 *     height. The percentage size change. For example, 1.25 makes the new image 125% of the size of
 *     the receiver. The scale factor 0.5 makes the new image 50% of the size of the receiver.
 *
 * @overload resize(cols, rows, filter, blur)
 *   @param cols [Numeric] The desired width
 *   @param rows [Numeric] The desired height.
 *   @param filter [Magick::FilterType] the filter type
 *   @param blur [Numeric] the blur size
 *
 * @return [Magick::Image] a new image
 * @see Image#resize!
 */
VALUE
Image_resize(int argc, VALUE *argv, VALUE self)
{
    rm_check_destroyed(self);
    return resize(False, argc, argv, self);
}


/**
 * Scale an image to the desired dimensions using the specified filter and blur factor.
 * In-place form of {Image#resize}.
 *
 * @overload resize!(scale)
 *   @param scale [Numeric] You can use this argument instead of specifying the desired width and
 *     height. The percentage size change. For example, 1.25 makes the new image 125% of the size of
 *     the receiver. The scale factor 0.5 makes the new image 50% of the size of the receiver.
 *
 * @overload resize!(cols, rows, filter, blur)
 *   @param cols [Numeric] The desired width
 *   @param rows [Numeric] The desired height.
 *   @param filter [Magick::FilterType] the filter type
 *   @param blur [Numeric] the blur size
 *
 * @return [Magick::Image] a new image
 * @see Image#resize!
 */
VALUE
Image_resize_bang(int argc, VALUE *argv, VALUE self)
{
    rm_check_frozen(self);
    return resize(True, argc, argv, self);
}


/**
 * Offset an image as defined by x_offset and y_offset.
 *
 * @param x_offset [Numeric] the x offset
 * @param y_offset [Numeric] the y offset
 * @return [Magick::Image] a new image
 */
VALUE
Image_roll(VALUE self, VALUE x_offset, VALUE y_offset)
{
    Image *image, *new_image;
    ExceptionInfo *exception;
    ssize_t x = NUM2LONG(x_offset);
    ssize_t y = NUM2LONG(y_offset);

    image = rm_check_destroyed(self);

    exception = AcquireExceptionInfo();
    GVL_STRUCT_TYPE(RollImage) args = { image, x, y, exception };
    new_image = (Image *)CALL_FUNC_WITHOUT_GVL(GVL_FUNC(RollImage), &args);
    rm_check_exception(exception, new_image, DestroyOnError);
    DestroyExceptionInfo(exception);

    return rm_image_new(new_image);
}


/**
 * Rotate the image.
 *
 * No Ruby usage (internal function)
 *
 * @param bang whether the bang (!) version of the method was called
 * @param argc number of input arguments
 * @param argv array of input arguments
 * @param self this object
 * @return self if bang, otherwise a new image
 * @see Image_rotate
 * @see Image_rotate_bang
 */
static VALUE
rotate(int bang, int argc, VALUE *argv, VALUE self)
{
    Image *image, *new_image;
    double degrees;
    char *arrow;
    size_t arrow_l;
    ExceptionInfo *exception;

    TypedData_Get_Struct(self, Image, &rm_image_data_type, image);

    switch (argc)
    {
        case 2:
            arrow = rm_str2cstr(argv[1], &arrow_l);
            if (arrow_l != 1 || (*arrow != '<' && *arrow != '>'))
            {
                rb_raise(rb_eArgError, "second argument must be '<' or '>', '%s' given", arrow);
            }
            if (*arrow == '>' && image->columns <= image->rows)
            {
                return Qnil;
            }
            if (*arrow == '<' && image->columns >= image->rows)
            {
                return Qnil;
            }
        case 1:
            degrees = NUM2DBL(argv[0]);
            break;
        default:
            rb_raise(rb_eArgError, "wrong number of arguments (%d for 1 or 2)", argc);
            break;
    }

    exception = AcquireExceptionInfo();

    GVL_STRUCT_TYPE(RotateImage) args = { image, degrees, exception };
    new_image = (Image *)CALL_FUNC_WITHOUT_GVL(GVL_FUNC(RotateImage), &args);
    rm_check_exception(exception, new_image, DestroyOnError);
    DestroyExceptionInfo(exception);

    if (bang)
    {
        rm_ensure_result(new_image);
        UPDATE_DATA_PTR(self, new_image);
        rm_image_destroy(image);
        return self;
    }
    return rm_image_new(new_image);
}


/**
 * Rotate the receiver by the specified angle. Positive angles rotate clockwise while negative
 * angles rotate counter-clockwise. New pixels introduced by the rotation are the same color as the
 * current background color. Set the background color to "none" to make the new pixels transparent
 * black.
 *
 * @overload rotate(degrees)
 *   @param degrees [Numeric] The number of degrees to rotate the image.
 *
 * @overload rotate(degrees, qualifier)
 *   @param degrees [Numeric] The number of degrees to rotate the image.
 *   @param qualifier [String] If present, either ">" or "<". If ">", rotates the image only if the
 *     image's width exceeds its height. If "<" rotates the image only if its height exceeds its
 *     width. If this argument is omitted the image is always rotated.
 *
 * @return [Magick::Image] a new image
 * @see Image#rotate!
 */
VALUE
Image_rotate(int argc, VALUE *argv, VALUE self)
{
    rm_check_destroyed(self);
    return rotate(False, argc, argv, self);
}


/**
 * Rotate the image.
 * In-place form of {Image#rotate}.
 *
 * @overload rotate!(degrees)
 *   @param degrees [Numeric] The number of degrees to rotate the image.
 *
 * @overload rotate!(degrees, qualifier)
 *   @param degrees [Numeric] The number of degrees to rotate the image.
 *   @param qualifier [String] If present, either ">" or "<". If ">", rotates the image only if the
 *     image's width exceeds its height. If "<" rotates the image only if its height exceeds its
 *     width. If this argument is omitted the image is always rotated.
 *
 * @return [Magick::Image] a new image
 * @see Image#rotate!
 */
VALUE
Image_rotate_bang(int argc, VALUE *argv, VALUE self)
{
    rm_check_frozen(self);
    return rotate(True, argc, argv, self);
}


/**
 * Return image rows.
 *
 * @return [Integer] the image rows
 */
VALUE
Image_rows(VALUE self)
{
    IMPLEMENT_TYPED_ATTR_READER(Image, rows, int, &rm_image_data_type);
}


/**
 * Scale an image to the desired dimensions with pixel sampling. Unlike other scaling methods, this
 * method does not introduce any additional color into the scaled image.
 *
 * @overload sample(scale)
 *   @param scale [Numeric] You can use this argument instead of specifying the desired width and
 *     height. The percentage size change. For example, 1.25 makes the new image 125% of the size of
 *     the receiver. The scale factor 0.5 makes the new image 50% of the size of the receiver.
 *
 * @overload sample(cols, rows)
 *   @param cols [Numeric] The desired width.
 *   @param rows [Numeric] The desired height.
 *
 * @return [Magick::Image] a new image
 * @see Image#sample!
 */
VALUE
Image_sample(int argc, VALUE *argv, VALUE self)
{
    rm_check_destroyed(self);
    return scale(False, argc, argv, self, GVL_FUNC(SampleImage));
}


/**
 * Scale an image to the desired dimensions with pixel sampling.
 * In-place form of {Image#sample}.
 *
 * @overload sample!(scale)
 *   @param scale [Numeric] You can use this argument instead of specifying the desired width and
 *     height. The percentage size change. For example, 1.25 makes the new image 125% of the size of
 *     the receiver. The scale factor 0.5 makes the new image 50% of the size of the receiver.
 *
 * @overload sample!(cols, rows)
 *   @param cols [Numeric] The desired width.
 *   @param rows [Numeric] The desired height.
 *
 * @return [Magick::Image] a new image
 * @see Image#sample
 */
VALUE
Image_sample_bang(int argc, VALUE *argv, VALUE self)
{
    rm_check_frozen(self);
    return scale(True, argc, argv, self, GVL_FUNC(SampleImage));
}


/**
 * Change the size of an image to the given dimensions. Alias of {Image#sample}.
 *
 * @overload scale(scale)
 *   @param scale [Float] You can use this argument instead of specifying the desired width and
 *     height. The percentage size change. For example, 1.25 makes the new image 125% of the size of
 *     the receiver. The scale factor 0.5 makes the new image 50% of the size of the receiver.
 *
 * @overload scale(cols, rows)
 *   @param cols [Numeric] The desired width.
 *   @param rows [Numeric] The desired height.
 *
 * @return [Magick::Image] a new image
 * @see Image#sample
 * @see Image#scale!
 */
VALUE
Image_scale(int argc, VALUE *argv, VALUE self)
{
    rm_check_destroyed(self);
    return scale(False, argc, argv, self, GVL_FUNC(ScaleImage));
}


/**
 * Change the size of an image to the given dimensions. Alias of {Image#sample!}.
 *
 * @overload scale!(scale)
 *   @param scale [Float] You can use this argument instead of specifying the desired width and
 *     height. The percentage size change. For example, 1.25 makes the new image 125% of the size of
 *     the receiver. The scale factor 0.5 makes the new image 50% of the size of the receiver.
 *
 * @overload scale!(cols, rows)
 *   @param cols [Numeric] The desired width.
 *   @param rows [Numeric] The desired height.
 *
 * @return [Magick::Image] a new image
 * @see Image#sample
 * @see Image#scale!
 */
VALUE
Image_scale_bang(int argc, VALUE *argv, VALUE self)
{
    rm_check_frozen(self);
    return scale(True, argc, argv, self, GVL_FUNC(ScaleImage));
}


// aliases for common use of structure types; SampleImage, ScaleImage,
typedef GVL_STRUCT_TYPE(SampleImage) GVL_STRUCT_TYPE(scale);

/**
 * Call ScaleImage or SampleImage
 *
 * Notes:
 *   - If 1 argument > 0, multiply current size by this much.
 *   - If 2 arguments, (cols, rows).
 *
 * No Ruby usage (internal function)
 *
 * @param bang whether the bang (!) version of the method was called
 * @param argc number of input arguments
 * @param argv array of input arguments
 * @param self this object
 * @param scaler which scalar to use (ScaleImage or SampleImage)
 * @return self if bang, otherwise a new image
 * @see Image_sample
 * @see Image_sample_bang
 * @see Image_scale
 * @see Image_scale_bang
 */
static VALUE
scale(int bang, int argc, VALUE *argv, VALUE self, gvl_function_t fp)
{
    Image *image, *new_image;
    unsigned long columns, rows;
    double scale_arg, drows, dcols;
    ExceptionInfo *exception;

    TypedData_Get_Struct(self, Image, &rm_image_data_type, image);

    switch (argc)
    {
        case 2:
            columns = NUM2ULONG(argv[0]);
            rows    = NUM2ULONG(argv[1]);
            if (columns == 0 || rows == 0)
            {
                rb_raise(rb_eArgError, "invalid result dimension (%lu, %lu given)", columns, rows);
            }
            break;
        case 1:
            scale_arg = NUM2DBL(argv[0]);
            if (scale_arg <= 0)
            {
                rb_raise(rb_eArgError, "invalid scale value (%g given)", scale_arg);
            }
            drows = scale_arg * image->rows + 0.5;
            dcols = scale_arg * image->columns + 0.5;
            if (drows > (double)ULONG_MAX || dcols > (double)ULONG_MAX)
            {
                rb_raise(rb_eRangeError, "resized image too big");
            }
            rows = (unsigned long) drows;
            columns = (unsigned long) dcols;
            break;
        default:
            rb_raise(rb_eArgError, "wrong number of arguments (%d for 1 or 2)", argc);
            break;
    }

    exception = AcquireExceptionInfo();
    GVL_STRUCT_TYPE(scale) args = { image, columns, rows, exception };
    new_image = (Image *)CALL_FUNC_WITHOUT_GVL(fp, &args);
    rm_check_exception(exception, new_image, DestroyOnError);
    DestroyExceptionInfo(exception);

    if (bang)
    {
        rm_ensure_result(new_image);
        UPDATE_DATA_PTR(self, new_image);
        rm_image_destroy(image);
        return self;
    }

    return rm_image_new(new_image);
}


/**
 * Return the scene number assigned to the image the last time the image was written to a
 * multi-image image file.
 *
 * @return [Integer] the image scene
 */
VALUE
Image_scene(VALUE self)
{
    IMPLEMENT_TYPED_ATTR_READER(Image, scene, ulong, &rm_image_data_type);
}


/**
 * Selectively blur pixels within a contrast threshold.
 *
 * @overload selective_blur_channel(radius, sigma, threshold, channel = Magick::AllChannels)
 *   @param radius [Numeric] the radius value
 *   @param sigma [Numeric] the sigma value
 *   @param threshold [Numeric, String] Either a number between 0.0 and 1.0 or a string in the form
 *     "NN%"
 *   @param channel [Magick::ChannelType] a ChannelType arguments.
 *
 * @overload selective_blur_channel(radius, sigma, threshold, *channels)
 *   @param radius [Numeric] the radius value
 *   @param sigma [Numeric] the sigma value
 *   @param threshold [Numeric, String] Either a number between 0.0 and 1.0 or a string in the form
 *     "NN%"
 *   @param *channels [Magick::ChannelType] one or more ChannelType arguments.
 *
 * @return [Magick::Image] a new image
 */
VALUE
Image_selective_blur_channel(int argc, VALUE *argv, VALUE self)
{
    Image *image, *new_image;
    double radius, sigma, threshold;
    ExceptionInfo *exception;
    ChannelType channels;

    image = rm_check_destroyed(self);
    channels = extract_channels(&argc, argv);
    if (argc > 3)
    {
        raise_ChannelType_error(argv[argc-1]);
    }
    if (argc != 3)
    {
        rb_raise(rb_eArgError, "wrong number of arguments (%d for 3 or more)", argc);
    }
    radius = NUM2DBL(argv[0]);
    sigma = NUM2DBL(argv[1]);

    // threshold is either a floating-point number or a string in the form "NN%".
    // Either way it's supposed to represent a percentage of the QuantumRange.
    threshold = rm_percentage(argv[2], 1.0) * QuantumRange;

    exception = AcquireExceptionInfo();
#if defined(IMAGEMAGICK_7)
    BEGIN_CHANNEL_MASK(image, channels);
    GVL_STRUCT_TYPE(SelectiveBlurImage) args = { image, radius, sigma, threshold, exception };
    new_image = (Image *)CALL_FUNC_WITHOUT_GVL(GVL_FUNC(SelectiveBlurImage), &args);
    CHANGE_RESULT_CHANNEL_MASK(new_image);
    END_CHANNEL_MASK(image);
#else
    GVL_STRUCT_TYPE(SelectiveBlurImageChannel) args = { image, channels, radius, sigma, threshold, exception };
    new_image = (Image *)CALL_FUNC_WITHOUT_GVL(GVL_FUNC(SelectiveBlurImageChannel), &args);
#endif
    rm_check_exception(exception, new_image, DestroyOnError);
    DestroyExceptionInfo(exception);

    return rm_image_new(new_image);
}


/**
 * Sets the depth of the image channel.
 *
 * @param channel_arg [Magick::ChannelType] the channel
 * @param depth [Numeric] the depth
 * @return self
 */
VALUE
Image_set_channel_depth(VALUE self, VALUE channel_arg, VALUE depth)
{
    Image *image;
    ChannelType channel;
    unsigned long channel_depth;
#if defined(IMAGEMAGICK_7)
    ExceptionInfo *exception;
#endif

    image = rm_check_frozen(self);

    VALUE_TO_ENUM(channel_arg, channel, ChannelType);
    channel_depth = NUM2ULONG(depth);

#if defined(IMAGEMAGICK_7)
    exception = AcquireExceptionInfo();
    BEGIN_CHANNEL_MASK(image, channel);
    GVL_STRUCT_TYPE(SetImageDepth) args = { image, channel_depth, exception };
    CALL_FUNC_WITHOUT_GVL(GVL_FUNC(SetImageDepth), &args);
    END_CHANNEL_MASK(image);
    CHECK_EXCEPTION();
    DestroyExceptionInfo(exception);
#else
    GVL_STRUCT_TYPE(SetImageChannelDepth) args = { image, channel, channel_depth };
    CALL_FUNC_WITHOUT_GVL(GVL_FUNC(SetImageChannelDepth), &args);
    rm_check_image_exception(image, RetainOnError);
#endif

    return self;
}


/**
 * Constructs a grayscale image for each channel specified.
 *
 * @overload separate(channel = Magick::AllChannels)
 *   @param channel [Magick::ChannelType] a ChannelType arguments.
 *
 * @overload separate(*channels)
 *   @param *channels [Magick::ChannelType] one or more ChannelType arguments.
 *
 * @return [Magick::ImageList] a new ImageList
 */
VALUE
Image_separate(int argc, VALUE *argv, VALUE self)
{
    Image *image, *new_images;
    ChannelType channels = UndefinedChannel;
    ExceptionInfo *exception;

    image = rm_check_destroyed(self);
    channels = extract_channels(&argc, argv);

    // All arguments are ChannelType enums
    if (argc > 0)
    {
        raise_ChannelType_error(argv[argc-1]);
    }

    exception = AcquireExceptionInfo();
#if defined(IMAGEMAGICK_7)
    BEGIN_CHANNEL_MASK(image, channels);
    GVL_STRUCT_TYPE(SeparateImages) args = { image, exception };
    new_images = (Image *)CALL_FUNC_WITHOUT_GVL(GVL_FUNC(SeparateImages), &args);
    CHANGE_RESULT_CHANNEL_MASK(new_images);
    END_CHANNEL_MASK(image);
#else
    GVL_STRUCT_TYPE(SeparateImages) args = { image, channels, exception };
    new_images = (Image *)CALL_FUNC_WITHOUT_GVL(GVL_FUNC(SeparateImages), &args);
#endif
    rm_check_exception(exception, new_images, DestroyOnError);
    DestroyExceptionInfo(exception);

    return rm_imagelist_from_images(new_images);
}


/**
 * Applies a special effect to the image, similar to the effect achieved in a photo darkroom by
 * sepia toning.
 *
 * @overload sepiatone(threshold = Magick::QuantumRange)
 *   @param threshold [Numeric] Threshold ranges from 0 to QuantumRange and is a measure of the extent
 *     of the sepia toning. A threshold of 80% is a good starting point for a reasonable tone.
 *   @return [Magick::Image] a new image
 */
VALUE
Image_sepiatone(int argc, VALUE *argv, VALUE self)
{
    Image *image, *new_image;
    double threshold = (double) QuantumRange;
    ExceptionInfo *exception;

    image = rm_check_destroyed(self);

    switch (argc)
    {
        case 1:
            threshold = NUM2DBL(argv[0]);
            break;
        case 0:
            break;
        default:
            rb_raise(rb_eArgError, "wrong number of arguments (%d for 0 or 1)", argc);
    }

    exception = AcquireExceptionInfo();
    GVL_STRUCT_TYPE(SepiaToneImage) args = { image, threshold, exception };
    new_image = (Image *)CALL_FUNC_WITHOUT_GVL(GVL_FUNC(SepiaToneImage), &args);
    rm_check_exception(exception, new_image, DestroyOnError);
    DestroyExceptionInfo(exception);

    return rm_image_new(new_image);
}


/**
 * Segments an image by analyzing the histograms of the color components and identifying units that
 * are homogeneous with the fuzzy c-means technique.
 *
 * @overload segment(colorspace = Magick::RGBColorspace, cluster_threshold = 1.0, smoothing_threshold = 1.5, verbose = false)
 *   @param colorspace [Magick::ColorspaceType] A ColorspaceType value. Empirical evidence suggests
 *     that distances in YUV or YIQ correspond to perceptual color differences more closely than do
 *     distances in RGB space. The image is then returned to RGB colorspace after color reduction.
 *   @param cluster_threshold [Numeric] The number of pixels in each cluster must exceed the the
 *     cluster threshold to be considered valid.
 *   @param smoothing_threshold [Numeric] The smoothing threshold eliminates noise in the second
 *     derivative of the histogram. As the value is increased, you can expect a smoother second
 *     derivative.
 *   @param verbose [Boolean] If true, segment prints detailed information about the identified classes.
 *   @return [Magick::Image] a new image
 */
VALUE
Image_segment(int argc, VALUE *argv, VALUE self)
{
    Image *image, *new_image;
    ColorspaceType colorspace   = RGBColorspace;    // These are the Magick++ defaults
    MagickBooleanType verbose   = MagickFalse;
    double cluster_threshold    = 1.0;
    double smoothing_threshold  = 1.5;
#if defined(IMAGEMAGICK_7)
    ExceptionInfo *exception;
#endif

    image = rm_check_destroyed(self);
    switch (argc)
    {
        case 4:
            verbose = (MagickBooleanType)RTEST(argv[3]);
        case 3:
            smoothing_threshold = NUM2DBL(argv[2]);
        case 2:
            cluster_threshold = NUM2DBL(argv[1]);
        case 1:
            VALUE_TO_ENUM(argv[0], colorspace, ColorspaceType);
        case 0:
            break;
        default:
            rb_raise(rb_eArgError, "wrong number of arguments (%d for 0 to 4)", argc);
            break;
    }

    new_image = rm_clone_image(image);

#if defined(IMAGEMAGICK_7)
    exception = AcquireExceptionInfo();
    GVL_STRUCT_TYPE(SegmentImage) args = { new_image, colorspace, verbose, cluster_threshold, smoothing_threshold, exception };
    CALL_FUNC_WITHOUT_GVL(GVL_FUNC(SegmentImage), &args);
    rm_check_exception(exception, new_image, DestroyOnError);
    DestroyExceptionInfo(exception);
#else
    GVL_STRUCT_TYPE(SegmentImage) args = { new_image, colorspace, verbose, cluster_threshold, smoothing_threshold };
    CALL_FUNC_WITHOUT_GVL(GVL_FUNC(SegmentImage), &args);
    rm_check_image_exception(new_image, DestroyOnError);
#endif

    return rm_image_new(new_image);
}


/**
 * If called with an associated block, properties runs the block once for each property defined for
 * the image. The block arguments are the property name and its value. If there is no block,
 * properties returns a hash with one element for each property. The hash key is the property name
 * and the associated value is the property value.
 *
 * @overload properties
 *   @return [Hash<String, String>] the properties
 *
 * @overload properties
 *   @yield [property, value]
 *   @yieldparam property [String] property key
 *   @yieldparam value [String] property value
 *   @return [Magick::Image] self
 */
VALUE
Image_properties(VALUE self)
{
    Image *image;
    VALUE attr_hash, ary;
    const char *property, *value;
#if defined(IMAGEMAGICK_7)
    ExceptionInfo *exception;
#endif

    image = rm_check_destroyed(self);

#if defined(IMAGEMAGICK_7)
    exception = AcquireExceptionInfo();
#endif

    if (rb_block_given_p())
    {
        long i;

        // Collect the properties first so that neither the ExceptionInfo nor
        // the property iterator is held across rb_yield. The block may break
        // or raise, which would otherwise leak the ExceptionInfo.
        ary = rb_ary_new();

        ResetImagePropertyIterator(image);
        property = GetNextImageProperty(image);
        while (property)
        {
#if defined(IMAGEMAGICK_7)
            value = GetImageProperty(image, property, exception);
#else
            value = GetImageProperty(image, property);
#endif
            rb_ary_push(ary, rb_assoc_new(rb_str_new2(property), value ? rb_str_new2(value) : Qnil));
            property = GetNextImageProperty(image);
        }
#if defined(IMAGEMAGICK_7)
        CHECK_EXCEPTION();
        DestroyExceptionInfo(exception);
#else
        rm_check_image_exception(image, RetainOnError);
#endif

        for (i = 0; i < RARRAY_LEN(ary); i++)
        {
            rb_yield(rb_ary_entry(ary, i));
        }

        RB_GC_GUARD(ary);

        return self;
    }

    // otherwise return properties hash
    else
    {
        attr_hash = rb_hash_new();
        ResetImagePropertyIterator(image);
        property = GetNextImageProperty(image);
        while (property)
        {
#if defined(IMAGEMAGICK_7)
            value = GetImageProperty(image, property, exception);
#else
            value = GetImageProperty(image, property);
#endif
            rb_hash_aset(attr_hash, rb_str_new2(property), value ? rb_str_new2(value) : Qnil);
            property = GetNextImageProperty(image);
        }
#if defined(IMAGEMAGICK_7)
        CHECK_EXCEPTION();
        DestroyExceptionInfo(exception);
#else
        rm_check_image_exception(image, RetainOnError);
#endif

        RB_GC_GUARD(attr_hash);

        return attr_hash;
    }

}


/**
 * Shine a distant light on an image to create a three-dimensional effect. You control the
 * positioning of the light with azimuth and elevation; azimuth is measured in degrees off the x
 * axis and elevation is measured in pixels above the Z axis.
 *
 * @overload shade(shading = false, azimuth = 30.0, elevation = 30.0)
 *   @param shading [Boolean] If true, shade shades the intensity of each pixel.
 *   @param azimuth [Numeric] The light source direction. The azimuth is measured in degrees. 0 is at
 *     9 o'clock. Increasing values move the light source counter-clockwise.
 *   @param elevation [Numeric] The light source direction. The azimuth is measured in degrees. 0 is
 *     at 9 o'clock. Increasing values move the light source counter-clockwise.
 *   @return [Magick::Image] a new image
 */
VALUE
Image_shade(int argc, VALUE *argv, VALUE self)
{
    Image *image, *new_image;
    double azimuth = 30.0, elevation = 30.0;
    MagickBooleanType shading = MagickFalse;
    ExceptionInfo *exception;

    image = rm_check_destroyed(self);
    switch (argc)
    {
        case 3:
            elevation = NUM2DBL(argv[2]);
        case 2:
            azimuth = NUM2DBL(argv[1]);
        case 1:
            shading = (MagickBooleanType)RTEST(argv[0]);
        case 0:
            break;
        default:
            rb_raise(rb_eArgError, "wrong number of arguments (%d for 0 to 3)", argc);
            break;
    }

    exception = AcquireExceptionInfo();
    GVL_STRUCT_TYPE(ShadeImage) args = { image, shading, azimuth, elevation, exception };
    new_image = (Image *)CALL_FUNC_WITHOUT_GVL(GVL_FUNC(ShadeImage), &args);
    rm_check_exception(exception, new_image, DestroyOnError);
    DestroyExceptionInfo(exception);

    return rm_image_new(new_image);
}


/**
 * Call ShadowImage. X- and y-offsets are the pixel offset. Alpha is either a number between 0 and 1
 * or a string "NN%". Sigma is the std. dev. of the Gaussian, in pixels.
 *
 * @overload Image#shadow(x_offset = 4, y_offset = 4, sigma = 4.0, alpha = 1.0)
 *   @param x_offset [Numeric] The shadow x-offset
 *   @param y_offset [Numeric] The shadow y-offset
 *   @param sigma [Numeric] The standard deviation of the Gaussian operator used to produce the
 *     shadow. The higher the number, the "blurrier" the shadow, but the longer it takes to produce
 *     the shadow. Must be > 0.0.
 *   @param alpha [Numeric, String] The percent alpha of the shadow. The argument may be a
 *     floating-point numeric value or a string in the form "NN%".
 *   @return [Magick::Image] a new image
 */
VALUE
Image_shadow(int argc, VALUE *argv, VALUE self)
{
    Image *image, *new_image;
    double alpha = 100.0;
    double sigma = 4.0;
    long x_offset = 4L;
    long y_offset = 4L;
    ExceptionInfo *exception;

    image = rm_check_destroyed(self);
    switch (argc)
    {
        case 4:
            alpha = rm_percentage(argv[3], 1.0);   // Clamp to 1.0 < x <= 100.0
            if (fabs(alpha) < 0.01)
            {
                rb_warning("shadow will be transparent - alpha %g very small", alpha);
            }
            alpha = FMIN(alpha, 1.0);
            alpha = FMAX(alpha, 0.01);
            alpha *= 100.0;
        case 3:
            sigma = NUM2DBL(argv[2]);
        case 2:
            y_offset = NUM2LONG(argv[1]);
        case 1:
            x_offset = NUM2LONG(argv[0]);
        case 0:
            break;
        default:
            rb_raise(rb_eArgError, "wrong number of arguments (%d for 0 to 4)", argc);
            break;
    }

    exception = AcquireExceptionInfo();
    GVL_STRUCT_TYPE(ShadowImage) args = { image, alpha, sigma, x_offset, y_offset, exception };
    new_image = (Image *)CALL_FUNC_WITHOUT_GVL(GVL_FUNC(ShadowImage), &args);
    rm_check_exception(exception, new_image, DestroyOnError);
    DestroyExceptionInfo(exception);

    return rm_image_new(new_image);
}


/**
 * Sharpen an image.
 *
 * @overload sharpen(radius = 0.0, sigma = 1.0)
 *   @param radius [Numeric] The radius of the Gaussian operator.
 *   @param sigma [Numeric] The sigma (standard deviation) of the Gaussian operator.
 *   @return [Magick::Image] a new image
 */
VALUE
Image_sharpen(int argc, VALUE *argv, VALUE self)
{
    return effect_image(self, argc, argv, GVL_FUNC(SharpenImage));
}


/**
 * Sharpen image on a channel.
 *
 * @overload sharpen_channel(radius = 0.0, sigma = 1.0, channel = Magick::AllChannels)
 *   @param radius [Numeric] The radius of the Gaussian operator.
 *   @param sigma [Numeric] The sigma (standard deviation) of the Gaussian operator.
 *   @param channel [Magick::ChannelType] a ChannelType arguments.
 *
 * @overload sharpen_channel(radius = 0.0, sigma = 1.0, *channels)
 *   @param radius [Numeric] The radius of the Gaussian operator.
 *   @param sigma [Numeric] The sigma (standard deviation) of the Gaussian operator.
 *   @param *channels [Magick::ChannelType] one or more ChannelType arguments.
 *
 * @return [Magick::Image] a new image
 */
VALUE
Image_sharpen_channel(int argc, VALUE *argv, VALUE self)
{
    Image *image, *new_image;
    ChannelType channels;
    ExceptionInfo *exception;
    double radius = 0.0, sigma = 1.0;

    image = rm_check_destroyed(self);
    channels = extract_channels(&argc, argv);

    // There must be 0, 1, or 2 remaining arguments.
    switch (argc)
    {
        case 2:
            sigma = NUM2DBL(argv[1]);
            /* Fall thru */
        case 1:
            radius = NUM2DBL(argv[0]);
            /* Fall thru */
        case 0:
            break;
        default:
            raise_ChannelType_error(argv[argc-1]);
    }

    exception = AcquireExceptionInfo();
#if defined(IMAGEMAGICK_7)
    BEGIN_CHANNEL_MASK(image, channels);
    GVL_STRUCT_TYPE(SharpenImage) args = { image, radius, sigma, exception };
    new_image = (Image *)CALL_FUNC_WITHOUT_GVL(GVL_FUNC(SharpenImage), &args);
    CHANGE_RESULT_CHANNEL_MASK(new_image);
    END_CHANNEL_MASK(image);
#else
    GVL_STRUCT_TYPE(SharpenImageChannel) args = { image, channels, radius, sigma, exception };
    new_image = (Image *)CALL_FUNC_WITHOUT_GVL(GVL_FUNC(SharpenImageChannel), &args);
#endif

    rm_check_exception(exception, new_image, DestroyOnError);
    DestroyExceptionInfo(exception);

    return rm_image_new(new_image);
}


/**
 * Shave pixels from the image edges, leaving a rectangle of the specified width & height in the
 * center.
 *
 * @param width [Numeric] the width to leave
 * @param height [Numeric] the hight to leave
 * @return [Magick::Image] a new image
 * @see Image#shave!
 */
VALUE
Image_shave(VALUE self, VALUE width, VALUE height)
{
    rm_check_destroyed(self);
    return xform_image(False, self, INT2FIX(0), INT2FIX(0), width, height, GVL_FUNC(ShaveImage));
}


/**
 * Shave pixels from the image edges, leaving a rectangle of the specified width & height in the
 * center.
 * In-place form of {Image#shave}.
 *
 * @param width [Numeric] the width to leave
 * @param height [Numeric] the hight to leave
 * @return [Magick::Image] a new image
 * @see Image#shave
 */
VALUE
Image_shave_bang(VALUE self, VALUE width, VALUE height)
{
    rm_check_frozen(self);
    return xform_image(True, self, INT2FIX(0), INT2FIX(0), width, height, GVL_FUNC(ShaveImage));
}


/**
 * Shearing slides one edge of an image along the X or Y axis, creating a parallelogram. An X
 * direction shear slides an edge along the X axis, while a Y direction shear slides an edge along
 * the Y axis. The amount of the shear is controlled by a shear angle. For X direction shears,
 * x_shear is measured relative to the Y axis, and similarly, for Y direction shears y_shear is
 * measured relative to the X axis. Empty triangles left over from shearing the image are filled
 * with the background color.
 *
 * @param x_shear [Numeric] the x shear (in degrees)
 * @param y_shear [Numeric] the y shear (in degrees)
 * @return [Magick::Image] a new image
 */
VALUE
Image_shear(VALUE self, VALUE x_shear, VALUE y_shear)
{
    Image *image, *new_image;
    ExceptionInfo *exception;
    double x = NUM2DBL(x_shear);
    double y = NUM2DBL(y_shear);

    image = rm_check_destroyed(self);

    exception = AcquireExceptionInfo();
    GVL_STRUCT_TYPE(ShearImage) args = { image, x, y, exception };
    new_image = (Image *)CALL_FUNC_WITHOUT_GVL(GVL_FUNC(ShearImage), &args);
    rm_check_exception(exception, new_image, DestroyOnError);
    DestroyExceptionInfo(exception);

    return rm_image_new(new_image);
}


/**
 * Adjusts the contrast of an image channel with a non-linear sigmoidal contrast
 * algorithm. Increases the contrast of the image using a sigmoidal transfer function without
 * saturating highlights or shadows.
 *
 * @overload sigmoidal_contrast_channel(contrast = 3.0, midpoint = 50.0, sharpen = false, channel = Magick::AllChannels)
 *   @param contrast [Numeric] indicates how much to increase the contrast
 *     (0 is none; 3 is typical; 20 is pushing it)
 *   @param midpoint [Numeric] indicates where midtones fall in the resultant image (0 is white; 50%
 *     is middle-gray; 100% is black). Note that "50%" means "50% of the quantum range." This argument
 *     is a number between 0 and QuantumRange. To specify "50%" use QuantumRange * 0.50.
 *   @param sharpen [Boolean] Set sharpen to true to increase the image contrast otherwise the
 *     contrast is reduced.
 *   @param channel [Magick::ChannelType] a ChannelType arguments.
 *
 * @overload sigmoidal_contrast_channel(contrast = 3.0, midpoint = 50.0, sharpen = false, *channels)
 *   @param contrast [Numeric] indicates how much to increase the contrast
 *     (0 is none; 3 is typical; 20 is pushing it)
 *   @param midpoint [Numeric] indicates where midtones fall in the resultant image (0 is white; 50%
 *     is middle-gray; 100% is black). Note that "50%" means "50% of the quantum range." This argument
 *     is a number between 0 and QuantumRange. To specify "50%" use QuantumRange * 0.50.
 *   @param sharpen [Boolean] Set sharpen to true to increase the image contrast otherwise the
 *     contrast is reduced.
 *   @param *channels [Magick::ChannelType] one or more ChannelType arguments.
 *
 * @return [Magick::Image] a new image
 */
VALUE
Image_sigmoidal_contrast_channel(int argc, VALUE *argv, VALUE self)
{
    Image *image, *new_image;
    MagickBooleanType sharpen = MagickFalse;
    double contrast = 3.0;
    double midpoint = 50.0;
    ChannelType channels;
#if defined(IMAGEMAGICK_7)
    ExceptionInfo *exception;
#endif

    image = rm_check_destroyed(self);
    channels = extract_channels(&argc, argv);

    switch (argc)
    {
        case 3:
            sharpen  = (MagickBooleanType) RTEST(argv[2]);
        case 2:
            midpoint = NUM2DBL(argv[1]);
        case 1:
            contrast = NUM2DBL(argv[0]);
        case 0:
            break;
        default:
            raise_ChannelType_error(argv[argc-1]);
            break;
    }

    new_image = rm_clone_image(image);

#if defined(IMAGEMAGICK_7)
    exception = AcquireExceptionInfo();
    BEGIN_CHANNEL_MASK(new_image, channels);
    GVL_STRUCT_TYPE(SigmoidalContrastImage) args = { new_image, sharpen, contrast, midpoint, exception };
    CALL_FUNC_WITHOUT_GVL(GVL_FUNC(SigmoidalContrastImage), &args);
    END_CHANNEL_MASK(new_image);
    rm_check_exception(exception, new_image, DestroyOnError);
    DestroyExceptionInfo(exception);
#else
    GVL_STRUCT_TYPE(SigmoidalContrastImageChannel) args = { new_image, channels, sharpen, contrast, midpoint };
    CALL_FUNC_WITHOUT_GVL(GVL_FUNC(SigmoidalContrastImageChannel), &args);
    rm_check_image_exception(new_image, DestroyOnError);
#endif

    return rm_image_new(new_image);
}


/**
 * Compute a message digest from an image pixel stream with an implementation of the NIST SHA-256
 * Message Digest algorithm.
 *
 * @return [String, nil] the message digest
 */
VALUE
Image_signature(VALUE self)
{
    Image *image;
    const char *signature;
#if defined(IMAGEMAGICK_7)
    ExceptionInfo *exception;
#endif

    image = rm_check_destroyed(self);

#if defined(IMAGEMAGICK_7)
    exception = AcquireExceptionInfo();
    GVL_STRUCT_TYPE(SignatureImage) args = { image, exception };
    CALL_FUNC_WITHOUT_GVL(GVL_FUNC(SignatureImage), &args);
    CHECK_EXCEPTION();
    DestroyExceptionInfo(exception);
#else
    GVL_STRUCT_TYPE(SignatureImage) args = { image };
    CALL_FUNC_WITHOUT_GVL(GVL_FUNC(SignatureImage), &args);
    rm_check_image_exception(image, RetainOnError);
#endif
    signature = rm_get_property(image, "signature");
    if (!signature)
    {
        return Qnil;
    }
    return rb_str_new(signature, 64);
}


/**
 * Simulates a pencil sketch. For best results start with a grayscale image.
 *
 * @overload sketch(radius = 0.0, sigma = 1.0, angle = 0.0)
 *   @param radius [Numeric] The radius
 *   @param sigma [Numeric] The standard deviation
 *   @param angle [Numeric] The angle (in degrees)
 *   @return [Magick::Image] a new image
 *   @see motion_blur
 */
VALUE
Image_sketch(int argc, VALUE *argv, VALUE self)
{
    rm_check_destroyed(self);
    return motion_blur(argc, argv, self, GVL_FUNC(SketchImage));
}


/**
 * Apply a special effect to the image, similar to the effect achieved in a photo darkroom by
 * selectively exposing areas of photo sensitive paper to light. Threshold ranges from 0 to
 * QuantumRange and is a measure of the extent of the solarization.
 *
 * @overload solarize(threshold = 50.0)
 *   @param threshold [Numeric] Ranges from 0 to QuantumRange and is a measure of the extent of the
 *   solarization.
 *   @return a new image
 */
VALUE
Image_solarize(int argc, VALUE *argv, VALUE self)
{
    Image *image, *new_image;
    double threshold = 50.0;
#if defined(IMAGEMAGICK_7)
    ExceptionInfo *exception;
#endif

    image = rm_check_destroyed(self);
    switch (argc)
    {
        case 1:
            threshold = NUM2DBL(argv[0]);
            if (threshold < 0.0 || threshold > QuantumRange)
            {
                rb_raise(rb_eArgError, "threshold out of range, must be >= 0.0 and < QuantumRange");
            }
        case 0:
            break;
        default:
            rb_raise(rb_eArgError, "wrong number of arguments (%d for 0 or 1)", argc);
            break;
    }

    new_image = rm_clone_image(image);

#if defined(IMAGEMAGICK_7)
    exception = AcquireExceptionInfo();
    GVL_STRUCT_TYPE(SolarizeImage) args = { new_image, threshold, exception };
    CALL_FUNC_WITHOUT_GVL(GVL_FUNC(SolarizeImage), &args);
    rm_check_exception(exception, new_image, DestroyOnError);
    DestroyExceptionInfo(exception);
#else
    GVL_STRUCT_TYPE(SolarizeImage) args = { new_image, threshold };
    CALL_FUNC_WITHOUT_GVL(GVL_FUNC(SolarizeImage), &args);
    rm_check_image_exception(new_image, DestroyOnError);
#endif

    return rm_image_new(new_image);
}


/**
 * Compare two images.
 *
 * @param other [Object] other image
 * @return [-1, 0, 1, nil] the result of compare
 */
VALUE
Image_spaceship(VALUE self, VALUE other)
{
    Image *imageA, *imageB;
    const char *sigA, *sigB;
    int res;
#if defined(IMAGEMAGICK_7)
    ExceptionInfo *exception;
#endif

    imageA = rm_check_destroyed(self);

    // If the other object isn't a Image object, then they can't be equal.
    if (!rb_obj_is_kind_of(other, Class_Image))
    {
        return Qnil;
    }

    imageB = rm_check_destroyed(other);

#if defined(IMAGEMAGICK_7)
    exception = AcquireExceptionInfo();
    GVL_STRUCT_TYPE(SignatureImage) args1 = { imageA, exception };
    CALL_FUNC_WITHOUT_GVL(GVL_FUNC(SignatureImage), &args1);
    CHECK_EXCEPTION();
    GVL_STRUCT_TYPE(SignatureImage) args2 = { imageB, exception };
    CALL_FUNC_WITHOUT_GVL(GVL_FUNC(SignatureImage), &args2);
    CHECK_EXCEPTION();
    DestroyExceptionInfo(exception);
#else
    GVL_STRUCT_TYPE(SignatureImage) args1 = { imageA };
    CALL_FUNC_WITHOUT_GVL(GVL_FUNC(SignatureImage), &args1);
    GVL_STRUCT_TYPE(SignatureImage) args2 = { imageB };
    CALL_FUNC_WITHOUT_GVL(GVL_FUNC(SignatureImage), &args2);
#endif
    sigA = rm_get_property(imageA, "signature");
    sigB = rm_get_property(imageB, "signature");
    if (!sigA || !sigB)
    {
        rb_raise(Class_ImageMagickError, "can't get image signature");
    }

    res = memcmp(sigA, sigB, 64);
    res = res > 0 ? 1 : (res < 0 ? -1 :  0);    // reduce to 1, -1, 0

    return INT2FIX(res);
}


/**
 * Count the number of channels from the specified list are in an image. Note
 * that this method also removes invalid channels based on the image.
 *
 * No Ruby usage (internal function)
 *
 * @param image the image
 * @param channels the channels
 * @return number of channels
 */
static unsigned long
count_channels(Image *image, ChannelType *channels)
{
    unsigned long ncolors = 0UL;

    if (image->colorspace != CMYKColorspace)
    {
        *channels = (ChannelType) (*channels & ~IndexChannel);  /* remove index channels from count */
    }
#if defined(IMAGEMAGICK_7)
    if ( image->alpha_trait == UndefinedPixelTrait )
#else
    if ( image->matte == MagickFalse )
#endif
    {
        *channels = (ChannelType) (*channels & ~OpacityChannel);  /* remove matte/alpha *channels from count */
    }

    if (*channels & RedChannel)
    {
        ncolors += 1;
    }
    if (*channels & GreenChannel)
    {
        ncolors += 1;
    }
    if (*channels & BlueChannel)
    {
        ncolors += 1;
    }
    if (*channels & IndexChannel)
    {
        ncolors += 1;
    }
    if (*channels & OpacityChannel)
    {
        ncolors += 1;
    }

    return ncolors;
}


/**
 * Fills the image with the specified color or colors, starting at the x,y coordinates associated
 * with the color and using the specified interpolation method.
 *
 * @overload sparse_color(method, x1, y1, color)
 * @overload sparse_color(method, x1, y1, color, x2, y2, color)
 * @overload sparse_color(method, x1, y1, color, x2, y2, color, ...)
 * @overload sparse_color(method, x1, y1, color, channel)
 * @overload sparse_color(method, x1, y1, color, x2, y2, color, channel)
 * @overload sparse_color(method, x1, y1, color, x2, y2, color, ..., channel)
 * @overload sparse_color(method, x1, y1, color, channel, ...)
 * @overload sparse_color(method, x1, y1, color, x2, y2, color, channel, ...)
 * @overload sparse_color(method, x1, y1, color, x2, y2, color, ..., channel, ...)
 *   @param method [Magick::SparseColorMethod] the method
 *   @param x1 [Float] One or more x.
 *   @param y1 [Float] One or more y.
 *   @param color [Magick::Pixel, String] One or more color
 *   @param channel [Magick::ChannelType] one or more ChannelType arguments
 *
 * @return [Magick::Image] a new image
 */
VALUE
Image_sparse_color(int argc, VALUE *argv, VALUE self)
{
    Image *image, *new_image;
    unsigned long x, nargs, ncolors;
    SparseColorMethod method;
    int n, exp;
    double * volatile args;
    ChannelType channels;
    MagickPixel pp;
    ExceptionInfo *exception;

    image = rm_check_destroyed(self);

    n = argc;
    channels = extract_channels(&argc, argv);
    n -= argc;  // n is now the number of channel arguments

    // After the channel arguments have been removed, and not counting the first
    // (method) argument, the number of arguments should be a multiple of 3.
    if (argc < 4 || argc % 3 != 1)
    {
        exp = (argc + 2) / 3 * 3;
        exp = max(exp, 3);
        rb_raise(rb_eArgError, "wrong number of arguments (expected at least %d, got %d)", n+exp+1,  n+argc);
    }

    // Get the method from the argument list
    VALUE_TO_ENUM(argv[0], method, SparseColorMethod);
    argv += 1;
    argc -= 1;

    // A lot of the following code is based on SparseColorOption, in wand/mogrify.c
    ncolors = count_channels(image, &channels);
    nargs = (argc / 3) * (2 + ncolors);

    // Allocate args from Ruby's memory so that GC will collect it if one of
    // the type conversions below raises an exception.
    args = ALLOC_N(double, nargs);
    memset(args, 0, nargs * sizeof(double));

    x = 0;
    n = 0;
    while (n < argc)
    {
        VALUE elem1 = argv[n++];
        VALUE elem2 = argv[n++];
        if (rm_check_num2dbl(elem1) && rm_check_num2dbl(elem2))
        {
            args[x++] = NUM2DBL(elem1);
            args[x++] = NUM2DBL(elem2);
        }
        else
        {
            xfree((void *) args);
            rb_raise(rb_eTypeError, "type mismatch: %s and %s given", rb_class2name(CLASS_OF(elem1)), rb_class2name(CLASS_OF(elem2)));
        }
        Color_to_MagickPixel(NULL, &pp, argv[n++]);
        if (channels & RedChannel)
        {
            args[x++] = pp.red / QuantumRange;
        }
        if (channels & GreenChannel)
        {
            args[x++] = pp.green / QuantumRange;
        }
        if (channels & BlueChannel)
        {
            args[x++] = pp.blue / QuantumRange;
        }
        if (channels & IndexChannel)
        {
            args[x++] = pp.index / QuantumRange;
        }
        if (channels & OpacityChannel)
        {
#if defined(IMAGEMAGICK_7)
            args[x++] = pp.alpha / QuantumRange;
#else
            args[x++] = pp.opacity / QuantumRange;
#endif
        }
    }

    exception = AcquireExceptionInfo();
#if defined(IMAGEMAGICK_7)
    BEGIN_CHANNEL_MASK(image, channels);
    GVL_STRUCT_TYPE(SparseColorImage) args_SparseColorImage = { image, method, nargs, args, exception };
    new_image = (Image *)CALL_FUNC_WITHOUT_GVL(GVL_FUNC(SparseColorImage), &args_SparseColorImage);
    CHANGE_RESULT_CHANNEL_MASK(new_image);
    END_CHANNEL_MASK(image);
#else
    GVL_STRUCT_TYPE(SparseColorImage) args_SparseColorImage = { image, channels, method, nargs, args, exception };
    new_image = (Image *)CALL_FUNC_WITHOUT_GVL(GVL_FUNC(SparseColorImage), &args_SparseColorImage);
#endif
    xfree((void *) args);
    rm_check_exception(exception, new_image, DestroyOnError);
    DestroyExceptionInfo(exception);

    return rm_image_new(new_image);
}


/**
 * Splice a solid color into the part of the image specified by the x, y, width,
 * and height arguments. If the color argument is specified it must be a color
 * name or Pixel.
 *
 * @overload splice(x, y, width, height, color = self.background_color)
 *   @param x [Numeric] Describe the rectangle to be spliced.
 *   @param y [Numeric] Describe the rectangle to be spliced.
 *   @param width [Numeric] Describe the rectangle to be spliced.
 *   @param height [Numeric] Describe the rectangle to be spliced.
 *   @param color [Magick::Pixel, String] The color to be spliced.
 *   @return [Magick::Image] a new image
 *   @see Image#chop
 */
VALUE
Image_splice(int argc, VALUE *argv, VALUE self)
{
    Image *image, *new_image;
    PixelColor color, old_color;
    RectangleInfo rectangle;
    ExceptionInfo *exception;

    image = rm_check_destroyed(self);

    switch (argc)
    {
        case 4:
            // use background color
            color = image->background_color;
            break;
        case 5:
            // Convert color argument to PixelColor
            Color_to_PixelColor(&color, argv[4]);
            break;
        default:
            rb_raise(rb_eArgError, "wrong number of arguments (%d for 4 or 5)", argc);
            break;
    }

    rectangle.x      = NUM2LONG(argv[0]);
    rectangle.y      = NUM2LONG(argv[1]);
    rectangle.width  = NUM2ULONG(argv[2]);
    rectangle.height = NUM2ULONG(argv[3]);

    exception = AcquireExceptionInfo();

    // Swap in color for the duration of this call.
    old_color = image->background_color;
    image->background_color = color;
    GVL_STRUCT_TYPE(SpliceImage) args = { image, &rectangle, exception };
    new_image = (Image *)CALL_FUNC_WITHOUT_GVL(GVL_FUNC(SpliceImage), &args);
    image->background_color = old_color;

    rm_check_exception(exception, new_image, DestroyOnError);
    DestroyExceptionInfo(exception);

    return rm_image_new(new_image);
}


/**
 * Randomly displace each pixel in a block defined by "radius".
 *
 * @overload spread(radius = 3.0)
 *   @param radius [Numeric] The radius
 *   @return [Magick::Image] a new image
 */
VALUE
Image_spread(int argc, VALUE *argv, VALUE self)
{
    Image *image, *new_image;
    double radius = 3.0;
    ExceptionInfo *exception;

    image = rm_check_destroyed(self);
    switch (argc)
    {
        case 1:
            radius = NUM2DBL(argv[0]);
        case 0:
            break;
        default:
            rb_raise(rb_eArgError, "wrong number of arguments (%d for 0 or 1)", argc);
            break;
    }

    exception = AcquireExceptionInfo();
#if defined(IMAGEMAGICK_7)
    GVL_STRUCT_TYPE(SpreadImage) args = { image, image->interpolate, radius, exception };
#else
    GVL_STRUCT_TYPE(SpreadImage) args = { image, radius, exception };
#endif
    new_image = (Image *)CALL_FUNC_WITHOUT_GVL(GVL_FUNC(SpreadImage), &args);
    rm_check_exception(exception, new_image, DestroyOnError);
    DestroyExceptionInfo(exception);

    return rm_image_new(new_image);
}


/**
 * Get the Boolean value that indicates the first image in an animation.
 *
 * @return [Boolean] true or false
 */
VALUE
Image_start_loop(VALUE self)
{
    IMPLEMENT_TYPED_ATTR_READER(Image, start_loop, boolean, &rm_image_data_type);
}

/**
 * Set the Boolean value that indicates the first image in an animation.
 *
 * @param val [Boolean] true or false
 * @return [Boolean] the given value
 */
VALUE
Image_start_loop_eq(VALUE self, VALUE val)
{
    IMPLEMENT_TYPED_ATTR_WRITER(Image, start_loop, boolean, &rm_image_data_type);
}


/**
 * Hides a digital watermark in the receiver. You can retrieve the watermark by reading the file
 * with the stegano: prefix, thereby proving the authenticity of the file.
 *
 * The watermarked image must be saved in a lossless RGB format such as MIFF, or PNG. You cannot
 * save a watermarked image in a lossy format such as JPEG or a pseudocolor format such as GIF. Once
 * written, the file must not be modified or processed in any way.
 *
 * @param watermark_image [Magick::Image, Magick::ImageList] Either an imagelist or an image
 * @param offset [Numeric] the start position within the image to hide the watermark.
 * @return [Magick::Image] a new image
 */
VALUE
Image_stegano(VALUE self, VALUE watermark_image, VALUE offset)
{
    Image *image, *new_image;
    VALUE wm_image;
    Image *watermark;
    ExceptionInfo *exception;

    image = rm_check_destroyed(self);

    wm_image = rm_cur_image(watermark_image);
    watermark = rm_check_destroyed(wm_image);

    image->offset = NUM2LONG(offset);

    exception = AcquireExceptionInfo();
    GVL_STRUCT_TYPE(SteganoImage) args = { image, watermark, exception };
    new_image = (Image *)CALL_FUNC_WITHOUT_GVL(GVL_FUNC(SteganoImage), &args);
    rm_check_exception(exception, new_image, DestroyOnError);

    DestroyExceptionInfo(exception);

    RB_GC_GUARD(wm_image);

    return rm_image_new(new_image);
}


/**
 * Combine two images and produces a single image that is the composite of a left and right image of
 * a stereo pair. Special red-green stereo glasses are required to view this effect.
 *
 * @param offset_image_arg [Magick::Image, Magick::ImageList] Either an imagelist or an image.
 * @return [Magick::Image] a new image
 */
VALUE
Image_stereo(VALUE self, VALUE offset_image_arg)
{
    Image *image, *new_image;
    VALUE offset_image;
    Image *offset;
    ExceptionInfo *exception;

    image = rm_check_destroyed(self);

    offset_image = rm_cur_image(offset_image_arg);
    offset = rm_check_destroyed(offset_image);

    exception = AcquireExceptionInfo();
    GVL_STRUCT_TYPE(StereoImage) args = { image, offset, exception };
    new_image = (Image *)CALL_FUNC_WITHOUT_GVL(GVL_FUNC(StereoImage), &args);
    rm_check_exception(exception, new_image, DestroyOnError);

    DestroyExceptionInfo(exception);

    RB_GC_GUARD(offset_image);

    return rm_image_new(new_image);
}


/**
 * Return the image's storage class (a.k.a. storage type, class type). If DirectClass then the
 * pixels contain valid RGB or CMYK colors.  If PseudoClass then the image has a colormap referenced
 * by the pixel's index member.
 *
 * @return [Magick::ClassType] the storage class
 */
VALUE
Image_class_type(VALUE self)
{
    Image *image = rm_check_destroyed(self);
    return ClassType_find(image->storage_class);
}


/**
 * Change the image's storage class.
 *
 * @param new_class_type [Magick::ClassType] the storage class
 * @return [Magick::ClassType] the given value
 */
VALUE
Image_class_type_eq(VALUE self, VALUE new_class_type)
{
    Image *image;
    ClassType class_type;
    QuantizeInfo qinfo;
#if defined(IMAGEMAGICK_7)
    ExceptionInfo *exception;
#endif

    image = rm_check_frozen(self);

    VALUE_TO_ENUM(new_class_type, class_type, ClassType);

    if (class_type == UndefinedClass)
    {
        rb_raise(rb_eArgError, "Invalid class type specified.");
    }

#if defined(IMAGEMAGICK_7)
    exception = AcquireExceptionInfo();
#endif

    if (image->storage_class == PseudoClass && class_type == DirectClass)
    {
#if defined(IMAGEMAGICK_7)
        GVL_STRUCT_TYPE(SyncImage) args = { image, exception };
        CALL_FUNC_WITHOUT_GVL(GVL_FUNC(SyncImage), &args);
        CHECK_EXCEPTION();
#else
        GVL_STRUCT_TYPE(SyncImage) args = { image };
        CALL_FUNC_WITHOUT_GVL(GVL_FUNC(SyncImage), &args);
#endif
        magick_free(image->colormap);
        image->colormap = NULL;
    }
    else if (image->storage_class == DirectClass && class_type == PseudoClass)
    {
        GetQuantizeInfo(&qinfo);
        qinfo.number_colors = QuantumRange+1;
#if defined(IMAGEMAGICK_7)
        GVL_STRUCT_TYPE(QuantizeImage) args = { &qinfo, image, exception };
        CALL_FUNC_WITHOUT_GVL(GVL_FUNC(QuantizeImage), &args);
        CHECK_EXCEPTION();
#else
        GVL_STRUCT_TYPE(QuantizeImage) args = { &qinfo, image };
        CALL_FUNC_WITHOUT_GVL(GVL_FUNC(QuantizeImage), &args);
#endif
    }

#if defined(IMAGEMAGICK_7)
    GVL_STRUCT_TYPE(SetImageStorageClass) args = { image, class_type, exception };
    CALL_FUNC_WITHOUT_GVL(GVL_FUNC(SetImageStorageClass), &args);
    CHECK_EXCEPTION();
    DestroyExceptionInfo(exception);
#else
    GVL_STRUCT_TYPE(SetImageStorageClass) args = { image, class_type };
    CALL_FUNC_WITHOUT_GVL(GVL_FUNC(SetImageStorageClass), &args);
#endif
    return new_class_type;
}


/**
 * Replace the pixels in the specified rectangle with the pixels in the pixels array.
 *
 * - This is the complement of get_pixels. The array object returned by get_pixels is suitable for
 *   use as the "new_pixels" argument.
 *
 * @param x_arg [Numeric] x position of start of region
 * @param y_arg [Numeric] y position of start of region
 * @param cols_arg [Numeric] width of region
 * @param rows_arg [Numeric] height of region
 * @param new_pixels [Array<Magick::Pixel>] the replacing pixels
 * @return [Magick::Image] self
 */
VALUE
Image_store_pixels(VALUE self, VALUE x_arg, VALUE y_arg, VALUE cols_arg,
                   VALUE rows_arg, VALUE new_pixels)
{
    Image *image;
    Pixel *pixel;
    VALUE new_pixel;
    long n, size;
    long x, y;
    unsigned long cols, rows;
    MagickBooleanType okay;
    ExceptionInfo *exception;
#if defined(IMAGEMAGICK_7)
    Quantum *pixels;
#else
    PixelPacket *pixels;
#endif

    image = rm_check_destroyed(self);

    x = NUM2LONG(x_arg);
    y = NUM2LONG(y_arg);
    cols = NUM2ULONG(cols_arg);
    rows = NUM2ULONG(rows_arg);
    if (x < 0 || y < 0 || x+cols > image->columns || y+rows > image->rows)
    {
        rb_raise(rb_eRangeError, "geometry (%lux%lu%+ld%+ld) exceeds image bounds",
                 cols, rows, x, y);
    }

    size = (long)(cols * rows);
    new_pixels = rb_Array(new_pixels);
    rm_check_ary_len(new_pixels, size);

#if defined(IMAGEMAGICK_7)
    exception = AcquireExceptionInfo();
    GVL_STRUCT_TYPE(SetImageStorageClass) args = { image, DirectClass, exception };
    void *ret = CALL_FUNC_WITHOUT_GVL(GVL_FUNC(SetImageStorageClass), &args);
    okay = static_cast<MagickBooleanType>(reinterpret_cast<intptr_t &>(ret));
    CHECK_EXCEPTION();
    if (!okay)
    {
        DestroyExceptionInfo(exception);
        rb_raise(Class_ImageMagickError, "SetImageStorageClass failed. Can't store pixels.");
    }
#else
    GVL_STRUCT_TYPE(SetImageStorageClass) args = { image, DirectClass };
    void *ret = CALL_FUNC_WITHOUT_GVL(GVL_FUNC(SetImageStorageClass), &args);
    okay = static_cast<MagickBooleanType>(reinterpret_cast<intptr_t &>(ret));
    rm_check_image_exception(image, RetainOnError);
    if (!okay)
    {
        rb_raise(Class_ImageMagickError, "SetImageStorageClass failed. Can't store pixels.");
    }
    exception = AcquireExceptionInfo();
#endif

    // Get a pointer to the pixels. Replace the values with the PixelPackets
    // from the pixels argument.
    {
        GVL_STRUCT_TYPE(GetAuthenticPixels) args = { image, x, y, cols, rows, exception };
        void *ret = CALL_FUNC_WITHOUT_GVL(GVL_FUNC(GetAuthenticPixels), &args);
        pixels = reinterpret_cast<decltype(pixels)>(ret);
        CHECK_EXCEPTION();

        if (pixels)
        {
#if defined(IMAGEMAGICK_6)
            IndexPacket *indexes = GetAuthenticIndexQueue(image);
#endif
            for (n = 0; n < size; n++)
            {
                new_pixel = rb_ary_entry(new_pixels, n);
                if (CLASS_OF(new_pixel) != Class_Pixel)
                {
                    DestroyExceptionInfo(exception);
                    rb_raise(rb_eTypeError, "Item in array should be a Pixel.");
                }
                TypedData_Get_Struct(new_pixel, Pixel, &rm_pixel_data_type, pixel);
#if defined(IMAGEMAGICK_7)
                SetPixelRed(image,   pixel->red,   pixels);
                SetPixelGreen(image, pixel->green, pixels);
                SetPixelBlue(image,  pixel->blue,  pixels);
                SetPixelAlpha(image, pixel->alpha, pixels);
                SetPixelBlack(image, pixel->black, pixels);
                pixels += GetPixelChannels(image);
#else
                SetPixelRed(pixels, pixel->red);
                SetPixelGreen(pixels, pixel->green);
                SetPixelBlue(pixels, pixel->blue);
                SetPixelOpacity(pixels, pixel->opacity);
                if (indexes)
                {
                    SetPixelIndex(indexes + n, pixel->black);
                }
                pixels++;
#endif
            }
            GVL_STRUCT_TYPE(SyncAuthenticPixels) args = { image, exception };
            CALL_FUNC_WITHOUT_GVL(GVL_FUNC(SyncAuthenticPixels), &args);
            CHECK_EXCEPTION();
        }

        DestroyExceptionInfo(exception);
    }

    RB_GC_GUARD(new_pixel);

    return self;
}


/**
 * Strips an image of all profiles and comments.
 *
 * @return [Magick::Image] self
 */
VALUE
Image_strip_bang(VALUE self)
{
#if defined(IMAGEMAGICK_7)
    ExceptionInfo *exception;
#endif

    Image *image = rm_check_frozen(self);

#if defined(IMAGEMAGICK_7)
    exception = AcquireExceptionInfo();
    StripImage(image, exception);
    CHECK_EXCEPTION();
    DestroyExceptionInfo(exception);
#else
    StripImage(image);
    rm_check_image_exception(image, RetainOnError);
#endif
    return self;
}


/**
 * Swirl the pixels about the center of the image, where degrees indicates the sweep of the arc
 * through which each pixel is moved. You get a more dramatic effect as the degrees move from 1 to
 * 360.
 *
 * @param degrees_obj [Numeric] the degrees
 * @return [Magick::Image] a new image
 */
VALUE
Image_swirl(VALUE self, VALUE degrees_obj)
{
    Image *image, *new_image;
    ExceptionInfo *exception;
    double degrees = NUM2DBL(degrees_obj);

    image = rm_check_destroyed(self);

    exception = AcquireExceptionInfo();

#if defined(IMAGEMAGICK_7)
    GVL_STRUCT_TYPE(SwirlImage) args = { image, degrees, image->interpolate, exception };
#else
    GVL_STRUCT_TYPE(SwirlImage) args = { image, degrees, exception };
#endif
    new_image = (Image *)CALL_FUNC_WITHOUT_GVL(GVL_FUNC(SwirlImage), &args);
    rm_check_exception(exception, new_image, DestroyOnError);
    DestroyExceptionInfo(exception);

    return rm_image_new(new_image);
}


/**
 * Emulates Magick++'s floodFillTexture.
 *
 * If the FloodfillMethod method is specified, flood-fills texture across pixels starting at the
 * target pixel and matching the specified color.
 *
 * If the FillToBorderMethod method is specified, flood-fills 'texture across pixels starting at the
 * target pixel and stopping at pixels matching the specified color.'
 *
 * @param color_obj [Magick::Pixel, String] the color
 * @param texture_obj [Magick::Image, Magick::ImageList] the texture to fill
 * @param x_obj [Numeric] the x position
 * @param y_obj [Numeric] the y position
 * @param method_obj [Magick::PaintMethod] the method to call (FloodfillMethod or FillToBorderMethod)
 * @return [Magick::Image] a new image
 */
VALUE
Image_texture_flood_fill(VALUE self, VALUE color_obj, VALUE texture_obj,
                         VALUE x_obj, VALUE y_obj, VALUE method_obj)
{
    Image *image, *new_image;
    Image *texture_image;
    PixelColor color;
    VALUE texture;
    DrawInfo *draw_info;
    long x, y;
    PaintMethod method;
    MagickPixel color_mpp;
    MagickBooleanType invert;
#if defined(IMAGEMAGICK_7)
    ExceptionInfo *exception;
#endif

    image = rm_check_destroyed(self);

    Color_to_PixelColor(&color, color_obj);
    texture = rm_cur_image(texture_obj);
    texture_image = rm_check_destroyed(texture);

    x = NUM2LONG(x_obj);
    y = NUM2LONG(y_obj);

    if ((unsigned long)x > image->columns || (unsigned long)y > image->rows)
    {
        rb_raise(rb_eArgError, "target out of range. %ldx%ld given, image is %" RMIuSIZE "x%" RMIuSIZE "",
                 x, y, image->columns, image->rows);
    }

    VALUE_TO_ENUM(method_obj, method, PaintMethod);
    if (method != FillToBorderMethod && method != FloodfillMethod)
    {
        rb_raise(rb_eArgError, "paint method must be FloodfillMethod or "
                 "FillToBorderMethod (%d given)", (int)method);
    }

    draw_info = CloneDrawInfo(NULL, NULL);
    if (!draw_info)
    {
        rb_raise(rb_eNoMemError, "not enough memory to continue");
    }

    draw_info->fill_pattern = rm_clone_image(texture_image);
    new_image = rm_clone_image(image);


    rm_init_magickpixel(new_image, &color_mpp);
    if (method == FillToBorderMethod)
    {
        invert = MagickTrue;
        color_mpp.red   = (MagickRealType) image->border_color.red;
        color_mpp.green = (MagickRealType) image->border_color.green;
        color_mpp.blue  = (MagickRealType) image->border_color.blue;
    }
    else
    {
        invert = MagickFalse;
        color_mpp.red   = (MagickRealType) color.red;
        color_mpp.green = (MagickRealType) color.green;
        color_mpp.blue  = (MagickRealType) color.blue;
    }

#if defined(IMAGEMAGICK_7)
    exception = AcquireExceptionInfo();
    GVL_STRUCT_TYPE(FloodfillPaintImage) args = { new_image, draw_info, &color_mpp, x, y, invert, exception };
    CALL_FUNC_WITHOUT_GVL(GVL_FUNC(FloodfillPaintImage), &args);
    DestroyDrawInfo(draw_info);
    rm_check_exception(exception, new_image, DestroyOnError);
    DestroyExceptionInfo(exception);
#else
    GVL_STRUCT_TYPE(FloodfillPaintImage) args = { new_image, DefaultChannels, draw_info, &color_mpp, x, y, invert };
    CALL_FUNC_WITHOUT_GVL(GVL_FUNC(FloodfillPaintImage), &args);

    DestroyDrawInfo(draw_info);
    rm_check_image_exception(new_image, DestroyOnError);
#endif

    RB_GC_GUARD(texture);

    return rm_image_new(new_image);
}


/**
 * Change the value of individual pixels based on the intensity of each pixel compared to
 * threshold. The result is a high-contrast, two color image.
 *
 * @param threshold_obj [Numeric] the threshold
 * @return [Magick::Image] a new image
 */
VALUE
Image_threshold(VALUE self, VALUE threshold_obj)
{
    Image *image, *new_image;
    double threshold = NUM2DBL(threshold_obj);
#if defined(IMAGEMAGICK_7)
    ExceptionInfo *exception;
#endif

    image = rm_check_destroyed(self);
    new_image = rm_clone_image(image);

#if defined(IMAGEMAGICK_7)
    exception = AcquireExceptionInfo();
    GVL_STRUCT_TYPE(BilevelImage) args = { new_image, threshold, exception };
    CALL_FUNC_WITHOUT_GVL(GVL_FUNC(BilevelImage), &args);
    rm_check_exception(exception, new_image, DestroyOnError);
    DestroyExceptionInfo(exception);
#else
    GVL_STRUCT_TYPE(BilevelImageChannel) args = { new_image, DefaultChannels, threshold };
    CALL_FUNC_WITHOUT_GVL(GVL_FUNC(BilevelImageChannel), &args);
    rm_check_image_exception(new_image, DestroyOnError);
#endif

    return rm_image_new(new_image);
}


// aliases for common use of structure types; WhiteThresholdImage
typedef GVL_STRUCT_TYPE(WhiteThresholdImage) GVL_STRUCT_TYPE(threshold_image);

/**
 * Call one of the xxxxThresholdImage methods.
 *
 * No Ruby usage (internal function)
 *
 * @param argc number of input arguments
 * @param argv array of input arguments
 * @param self this object
 * @param thresholder which xxxxThresholdImage method to call
 * @return a new image
 */
static VALUE
threshold_image(int argc, VALUE *argv, VALUE self, gvl_function_t fp)
{
    Image *image, *new_image;
    double red, green, blue, alpha;
    char ctarg[200];
#if defined(IMAGEMAGICK_7)
    ExceptionInfo *exception;
#endif
    VALUE red_obj, green_obj, blue_obj, kwargs;

    image = rm_check_destroyed(self);

    rb_scan_args(argc, argv, "12:", &red_obj, &green_obj, &blue_obj, &kwargs);
    switch (argc)
    {
        case 4:
            red     = NUM2DBL(red_obj);
            green   = NUM2DBL(green_obj);
            blue    = NUM2DBL(blue_obj);
            alpha   = get_named_alpha_value(kwargs);
            snprintf(ctarg, sizeof(ctarg), "%f,%f,%f,%f", red, green, blue, QuantumRange - alpha);
            break;
        case 3:
            red     = NUM2DBL(red_obj);
            green   = NUM2DBL(green_obj);
            blue    = NUM2DBL(blue_obj);
            snprintf(ctarg, sizeof(ctarg), "%f,%f,%f", red, green, blue);
            break;
        case 2:
            red     = NUM2DBL(red_obj);
            green   = NUM2DBL(green_obj);
            snprintf(ctarg, sizeof(ctarg), "%f,%f", red, green);
            break;
        case 1:
            red     = NUM2DBL(red_obj);
            snprintf(ctarg, sizeof(ctarg), "%f", red);
            break;
        default:
            rb_raise(rb_eArgError, "wrong number of arguments (%d for 1 to 4)", argc);
    }

    new_image = rm_clone_image(image);

#if defined(IMAGEMAGICK_7)
    exception = AcquireExceptionInfo();
    GVL_STRUCT_TYPE(threshold_image) args = { new_image, ctarg, exception };
    CALL_FUNC_WITHOUT_GVL(fp, &args);
    rm_check_exception(exception, new_image, DestroyOnError);
    DestroyExceptionInfo(exception);
#else
    GVL_STRUCT_TYPE(threshold_image) args = { new_image, ctarg };
    CALL_FUNC_WITHOUT_GVL(fp, &args);
    rm_check_image_exception(new_image, DestroyOnError);
#endif

    return rm_image_new(new_image);
}


/**
 * Fast resize for thumbnail images.
 *
 * No Ruby usage (internal function)
 *
 * Notes:
 *   - Uses BoxFilter, blur attribute of input image
 *
 * @param bang whether the bang (!) version of the method was called
 * @param argc number of input arguments
 * @param argv array of input arguments
 * @param self this object
 * @return self if bang, otherwise a new image
 * @see Image_thumbnail
 * @see Image_thumbnail_bang
 */
static VALUE
thumbnail(int bang, int argc, VALUE *argv, VALUE self)
{
    Image *image, *new_image;
    unsigned long columns, rows;
    double scale_arg, drows, dcols;
    char image_geometry[MaxTextExtent];
    RectangleInfo geometry;
    ExceptionInfo *exception;

    TypedData_Get_Struct(self, Image, &rm_image_data_type, image);

    switch (argc)
    {
        case 2:
            columns = NUM2ULONG(argv[0]);
            rows = NUM2ULONG(argv[1]);
            if (columns == 0 || rows == 0)
            {
                rb_raise(rb_eArgError, "invalid result dimension (%lu, %lu given)", columns, rows);
            }
            break;
        case 1:
            scale_arg = NUM2DBL(argv[0]);
            if (scale_arg < 0.0)
            {
                rb_raise(rb_eArgError, "invalid scale value (%g given)", scale_arg);
            }
            drows = scale_arg * image->rows + 0.5;
            dcols = scale_arg * image->columns + 0.5;
            if (drows > (double)ULONG_MAX || dcols > (double)ULONG_MAX)
            {
                rb_raise(rb_eRangeError, "resized image too big");
            }
            rows = (unsigned long) drows;
            columns = (unsigned long) dcols;
            break;
        default:
            rb_raise(rb_eArgError, "wrong number of arguments (%d for 1 or 2)", argc);
            break;
    }

    snprintf(image_geometry, sizeof(image_geometry), "%lux%lu", columns, rows);

    exception = AcquireExceptionInfo();
    ParseRegionGeometry(image, image_geometry, &geometry, exception);
    rm_check_exception(exception, image, RetainOnError);

    GVL_STRUCT_TYPE(ThumbnailImage) args = { image, geometry.width, geometry.height, exception };
    new_image = (Image *)CALL_FUNC_WITHOUT_GVL(GVL_FUNC(ThumbnailImage), &args);
    rm_check_exception(exception, new_image, DestroyOnError);
    DestroyExceptionInfo(exception);

    if (bang)
    {
        rm_ensure_result(new_image);
        UPDATE_DATA_PTR(self, new_image);
        rm_image_destroy(image);
        return self;
    }

    return rm_image_new(new_image);
}


/**
 * The thumbnail method is a fast resizing method suitable for use when the size of the resulting
 * image is < 10% of the original.
 *
 * @overload thumbnail(scale)
 *   @param scale [Numeric] The desired size represented as a floating-point number. For example, to
 *     make a thumbnail that is 9.5% of the size of the original image, use 0.095.
 *
 * @overload thumbnail(cols, rows)
 *   @param cols [Numeric] The desired width in pixels.
 *   @param rows [Numeric] The desired height.
 *
 * @return [Magick::Image] a new image
 * @see Image#thumbnail!
 */
VALUE
Image_thumbnail(int argc, VALUE *argv, VALUE self)
{
    rm_check_destroyed(self);
    return thumbnail(False, argc, argv, self);
}


/**
 * The thumbnail method is a fast resizing method suitable for use when the size of the resulting
 * image is < 10% of the original.  In-place form of {Image#thumbnail}.
 *
 * @overload thumbnail!(scale)
 *   @param scale [Numeric] The desired size represented as a floating-point number. For example, to
 *     make a thumbnail that is 9.5% of the size of the original image, use 0.095.
 *
 * @overload thumbnail!(cols, rows)
 *   @param cols [Numeric] The desired width in pixels.
 *   @param rows [Numeric] The desired height.
 *
 * @return [Magick::Image] a new image
 * @see Image#thumbnail
 */
VALUE
Image_thumbnail_bang(int argc, VALUE *argv, VALUE self)
{
    rm_check_frozen(self);
    return thumbnail(True, argc, argv, self);
}


/**
 * Get the number of ticks per second.
 * This attribute is used in conjunction with the delay attribute to establish the amount of time
 * that must elapse between frames in an animation.The default is 100.
 *
 * @return [Integer] ticks per second
 */
VALUE
Image_ticks_per_second(VALUE self)
{
    Image *image = rm_check_destroyed(self);
    return INT2FIX(image->ticks_per_second);
}


/**
 * Set the number of ticks per second.
 * This attribute is used in conjunction with the delay attribute to establish the amount of time
 * that must elapse between frames in an animation.The default is 100.
 *
 * @param tps [Numeric] ticks per second
 * @return [Numeric] the given value
 */
VALUE
Image_ticks_per_second_eq(VALUE self, VALUE tps)
{
    Image *image = rm_check_frozen(self);
    image->ticks_per_second = NUM2ULONG(tps);
    return tps;
}


/**
 * Applies a color vector to each pixel in the image.
 *
 * - Alpha values are percentages: 0.10 -> 10%.
 *
 * @overload tint(tint, red_alpha, green_alpha = red_alpha, blue_alpha = red_alpha, alpha_alpha = 1.0)
 *   @param tint [Magick::Pixel, String] the color name
 *   @param red_alpha [Numeric] the red value
 *   @param green_alpha [Numeric] the green value
 *   @param blue_alpha [Numeric] the blue value
 *   @param alpha_alpha [Numeric] the alpha value
 *   @return a new image
 */
VALUE
Image_tint(int argc, VALUE *argv, VALUE self)
{
    Image *image, *new_image;
    PixelColor tint;
    double red_pct_opaque, green_pct_opaque, blue_pct_opaque;
    double alpha_pct_opaque = 1.0;
    char alpha[50];
    ExceptionInfo *exception;

    image = rm_check_destroyed(self);

    switch (argc)
    {
        case 2:
            red_pct_opaque   = NUM2DBL(argv[1]);
            green_pct_opaque = blue_pct_opaque = red_pct_opaque;
            break;
        case 3:
            red_pct_opaque   = NUM2DBL(argv[1]);
            green_pct_opaque = NUM2DBL(argv[2]);
            blue_pct_opaque  = red_pct_opaque;
            break;
        case 4:
            red_pct_opaque     = NUM2DBL(argv[1]);
            green_pct_opaque   = NUM2DBL(argv[2]);
            blue_pct_opaque    = NUM2DBL(argv[3]);
            break;
        case 5:
            red_pct_opaque     = NUM2DBL(argv[1]);
            green_pct_opaque   = NUM2DBL(argv[2]);
            blue_pct_opaque    = NUM2DBL(argv[3]);
            alpha_pct_opaque   = NUM2DBL(argv[4]);
            break;
        default:
            rb_raise(rb_eArgError, "wrong number of arguments (%d for 2 to 5)", argc);
            break;
    }

    if (red_pct_opaque < 0.0 || green_pct_opaque < 0.0
        || blue_pct_opaque < 0.0 || alpha_pct_opaque < 0.0)
    {
        rb_raise(rb_eArgError, "alpha percentages must be non-negative.");
    }

    snprintf(alpha, sizeof(alpha),
            "%g,%g,%g,%g", red_pct_opaque*100.0, green_pct_opaque*100.0,
            blue_pct_opaque*100.0, alpha_pct_opaque*100.0);

    Color_to_PixelColor(&tint, argv[0]);
    exception = AcquireExceptionInfo();

#if defined(IMAGEMAGICK_7)
    GVL_STRUCT_TYPE(TintImage) args = { image, alpha, &tint, exception };
#else
    GVL_STRUCT_TYPE(TintImage) args = { image, alpha, tint, exception };
#endif
    new_image = (Image *)CALL_FUNC_WITHOUT_GVL(GVL_FUNC(TintImage), &args);
    rm_check_exception(exception, new_image, DestroyOnError);
    DestroyExceptionInfo(exception);

    return rm_image_new(new_image);
}


/**
 * Return a "blob" (a String) from the image.
 *
 * - The magick member of the Image structure determines the format of the
 *   returned blob (GIG, JPEG,  PNG, etc.)
 *
 * @yield [info]
 * @yieldparam info [Magick::Image::Info]
 * @return [String] the blob
 * @see Image#from_blob
 */
VALUE
Image_to_blob(VALUE self)
{
    Image *image;
    Info *info;
    const MagickInfo *magick_info;
    VALUE info_obj;
    VALUE blob_str;
    void *blob = NULL;
    size_t length = 2048;       // Do what Magick++ does
    ExceptionInfo *exception;

    // The user can specify the depth (8 or 16, if the format supports
    // both) and the image format by setting the depth and format
    // values in the info parm block.
    info_obj = rm_info_new();
    TypedData_Get_Struct(info_obj, Info, &rm_info_data_type, info);

    image = rm_check_destroyed(self);

    exception = AcquireExceptionInfo();

    // Copy the depth and magick fields to the Image
    if (info->depth != 0)
    {
#if defined(IMAGEMAGICK_7)
        GVL_STRUCT_TYPE(SetImageDepth) args = { image, info->depth, exception };
        CALL_FUNC_WITHOUT_GVL(GVL_FUNC(SetImageDepth), &args);
        CHECK_EXCEPTION();
#else
        GVL_STRUCT_TYPE(SetImageDepth) args = { image, info->depth };
        CALL_FUNC_WITHOUT_GVL(GVL_FUNC(SetImageDepth), &args);
        rm_check_image_exception(image, RetainOnError);
#endif
    }

    if (*info->magick)
    {
        SetImageInfo(info, MagickTrue, exception);
        CHECK_EXCEPTION();

        if (*info->magick == '\0')
        {
            return Qnil;
        }
        strlcpy(image->magick, info->magick, sizeof(image->magick));
    }

    // Fix #2844 - libjpeg exits when image is 0x0
    magick_info = GetMagickInfo(image->magick, exception);
    CHECK_EXCEPTION();

    if (magick_info)
    {
        if (  (!rm_strcasecmp(magick_info->name, "JPEG")
               || !rm_strcasecmp(magick_info->name, "JPG"))
              && (image->rows == 0 || image->columns == 0))
        {
            rb_raise(rb_eRuntimeError, "Can't convert %" RMIuSIZE "x%" RMIuSIZE " %.4s image to a blob",
                     image->columns, image->rows, magick_info->name);
        }
    }

    rm_sync_image_options(image, info);

    GVL_STRUCT_TYPE(ImageToBlob) args = { info, image, &length, exception };
    blob = CALL_FUNC_WITHOUT_GVL(GVL_FUNC(ImageToBlob), &args);
    CHECK_EXCEPTION();

    DestroyExceptionInfo(exception);

    if (length == 0 || !blob)
    {
        return Qnil;
    }

    blob_str = rb_str_new((const char *)blob, length);

    magick_free((void*)blob);

    RB_GC_GUARD(info_obj);
    RB_GC_GUARD(blob_str);

    return blob_str;
}


/**
 * Return a color name for the color intensity specified by the Magick::Pixel argument.
 *
 * @param pixel_arg [Magick::Pixel, String] the pixel
 * @return [String] the color name
 */
VALUE
Image_to_color(VALUE self, VALUE pixel_arg)
{
    Image *image;
    PixelColor pixel;
    ExceptionInfo *exception;
    char name[MaxTextExtent];

    image = rm_check_destroyed(self);
    Color_to_PixelColor(&pixel, pixel_arg);
    exception = AcquireExceptionInfo();

#if defined(IMAGEMAGICK_7)
    pixel.depth = MAGICKCORE_QUANTUM_DEPTH;
    pixel.colorspace = image->colorspace;
#endif

    // QueryColorname returns False if the color represented by the PixelPacket
    // doesn't have a "real" name, just a sequence of hex digits. We don't care
    // about that.

    QueryColorname(image, &pixel, AllCompliance, name, exception);
    CHECK_EXCEPTION();

    DestroyExceptionInfo(exception);

    return rb_str_new2(name);

}


/**
 * Alias for {Image#number_colors}.
 *
 * @return [Integer] number of unique colors
 * @see Image#number_colors
 */
VALUE
Image_total_colors(VALUE self)
{
    return Image_number_colors(self);
}


/**
 * Return the total ink density for a CMYK image.
 *
 * @return [Float] the total ink density
 */
VALUE
Image_total_ink_density(VALUE self)
{
    Image *image;
    double density;
#if defined(IMAGEMAGICK_7)
    ExceptionInfo *exception;
#endif

    image = rm_check_destroyed(self);

#if defined(IMAGEMAGICK_7)
    exception = AcquireExceptionInfo();
    density = GetImageTotalInkDensity(image, exception);
    CHECK_EXCEPTION();
    DestroyExceptionInfo(exception);
#else
    density = GetImageTotalInkDensity(image);
    rm_check_image_exception(image, RetainOnError);
#endif

    return rb_float_new(density);
}


/**
 * Changes the opacity value of all the pixels that match color to the value specified by
 * opacity. By default the pixel must match exactly, but you can specify a tolerance level by
 * setting the fuzz attribute on the image.
 *
 * - Default alpha is Magick::TransparentAlpha.
 * - Can use Magick::OpaqueAlpha or Magick::TransparentAlpha, or any
 *   value >= 0 && <= QuantumRange.
 * - Use Image#fuzz= to define the tolerance level.
 *
 * @overload transparent(color, alpha: Magick::TransparentAlpha)
 *   @param color [Magick::Pixel, String] The color
 *   @param alpha alpha [Numeric] the alpha
 *   @return [Magick::Image] a new image
 */
VALUE
Image_transparent(int argc, VALUE *argv, VALUE self)
{
    Image *image, *new_image;
    MagickPixel color;
    Quantum alpha = TransparentAlpha;
    MagickBooleanType okay;
#if defined(IMAGEMAGICK_7)
    ExceptionInfo *exception;
#endif
    VALUE color_obj, kwargs;

    image = rm_check_destroyed(self);

    rb_scan_args(argc, argv, "1:", &color_obj, &kwargs);
    Color_to_MagickPixel(image, &color, color_obj);
    if (RTEST(kwargs))
    {
        alpha = get_named_alpha_value(kwargs);
    }

    new_image = rm_clone_image(image);

#if defined(IMAGEMAGICK_7)
    exception = AcquireExceptionInfo();
    GVL_STRUCT_TYPE(TransparentPaintImage) args = { new_image, &color, alpha, MagickFalse, exception };
    void *ret = CALL_FUNC_WITHOUT_GVL(GVL_FUNC(TransparentPaintImage), &args);
    okay = static_cast<MagickBooleanType>(reinterpret_cast<intptr_t &>(ret));
    rm_check_exception(exception, new_image, DestroyOnError);
    DestroyExceptionInfo(exception);
#else
    GVL_STRUCT_TYPE(TransparentPaintImage) args = { new_image, &color, (Quantum)(QuantumRange - alpha), MagickFalse };
    void *ret = CALL_FUNC_WITHOUT_GVL(GVL_FUNC(TransparentPaintImage), &args);
    okay = static_cast<MagickBooleanType>(reinterpret_cast<intptr_t &>(ret));
    rm_check_image_exception(new_image, DestroyOnError);
#endif
    if (!okay)
    {
        // Force exception
        DestroyImage(new_image);
        rm_magick_error("TransparentPaintImage failed with no explanation");
    }

    return rm_image_new(new_image);
}


/**
 * Changes the opacity value associated with any pixel between low and high to the value defined by
 * opacity.
 *
 * As there is one fuzz value for the all the channels, the transparent method is not suitable for
 * the operations like chroma, where the tolerance for similarity of two color components (RGB) can
 * be different, Thus we define this method take two target pixels (one low and one high) and all
 * the pixels of an image which are lying between these two pixels are made transparent.
 *
 * @overload transparent_chroma(low, high, invert, alpha: Magick::TransparentAlpha)
 *   @param low [Magick::Pixel, String] The low ends of the pixel range
 *   @param high [Magick::Pixel, String] The high ends of the pixel range
 *   @param invert [Boolean] If true, all pixels outside the range are set to opacity.
 *   @param alpha [Numeric] The desired alpha.
 *   @return [Magick::Image] a new image
 */
VALUE
Image_transparent_chroma(int argc, VALUE *argv, VALUE self)
{
    Image *image, *new_image;
    Quantum alpha = TransparentAlpha;
    MagickPixel low, high;
    MagickBooleanType invert = MagickFalse;
    MagickBooleanType okay;
#if defined(IMAGEMAGICK_7)
    ExceptionInfo *exception;
#endif
    VALUE low_obj, high_obj, invert_obj, kwargs;

    image = rm_check_destroyed(self);

    rb_scan_args(argc, argv, "21:", &low_obj, &high_obj, &invert_obj, &kwargs);

    Color_to_MagickPixel(image, &low, low_obj);
    Color_to_MagickPixel(image, &high, high_obj);
    if (invert_obj != Qnil)
    {
        invert = (MagickBooleanType)RTEST(invert_obj);
    }
    if (RTEST(kwargs))
    {
        alpha = get_named_alpha_value(kwargs);
    }

    new_image = rm_clone_image(image);

#if defined(IMAGEMAGICK_7)
    exception = AcquireExceptionInfo();
    GVL_STRUCT_TYPE(TransparentPaintImageChroma) args = { new_image, &low, &high, alpha, invert, exception };
    void *ret = CALL_FUNC_WITHOUT_GVL(GVL_FUNC(TransparentPaintImageChroma), &args);
    okay = static_cast<MagickBooleanType>(reinterpret_cast<intptr_t &>(ret));
    rm_check_exception(exception, new_image, DestroyOnError);
    DestroyExceptionInfo(exception);
#else
    GVL_STRUCT_TYPE(TransparentPaintImageChroma) args = { new_image, &low, &high, (Quantum)(QuantumRange - alpha), invert };
    void *ret = CALL_FUNC_WITHOUT_GVL(GVL_FUNC(TransparentPaintImageChroma), &args);
    okay = static_cast<MagickBooleanType>(reinterpret_cast<intptr_t &>(ret));
    rm_check_image_exception(new_image, DestroyOnError);
#endif
    if (!okay)
    {
        // Force exception
        DestroyImage(new_image);
        rm_magick_error("TransparentPaintImageChroma failed with no explanation");
    }

    return rm_image_new(new_image);
}


/**
 * Return the name of the transparent color as a String.
 *
 * @return [String] the name of the transparent color
 */
VALUE
Image_transparent_color(VALUE self)
{
    Image *image = rm_check_destroyed(self);
    return rm_pixelcolor_to_color_name(image, &image->transparent_color);
}


/**
 * Set the the transparent color to the specified color spec.
 *
 * @param color [Magick::Pixel, String] the transparent color
 * @return [Magick::Pixel, String] the given color
 */
VALUE
Image_transparent_color_eq(VALUE self, VALUE color)
{
    Image *image = rm_check_frozen(self);
    Color_to_PixelColor(&image->transparent_color, color);
    return color;
}


/**
 * Creates a horizontal mirror image by reflecting the pixels around the central y-axis while
 * rotating them by 90 degrees.
 *
 * @return [Magick::Image] a new image
 * @see Image#transpose!
 */
VALUE
Image_transpose(VALUE self)
{
    rm_check_destroyed(self);
    return crisscross(False, self, GVL_FUNC(TransposeImage));
}


/**
 * Creates a horizontal mirror image by reflecting the pixels around the central y-axis while
 * rotating them by 90 degrees.
 * In-place form of {Image#transpose}.
 *
 * @return [Magick::Image] self
 * @see Image#transpose
 */
VALUE
Image_transpose_bang(VALUE self)
{
    rm_check_frozen(self);
    return crisscross(True, self, GVL_FUNC(TransposeImage));
}


/**
 * Creates a vertical mirror image by reflecting the pixels around the central x-axis while rotating
 * them by 270 degrees
 *
 * @return [Magick::Image] a new image
 * @see Image#transverse!
 */
VALUE
Image_transverse(VALUE self)
{
    rm_check_destroyed(self);
    return crisscross(False, self, GVL_FUNC(TransverseImage));
}

/**
 * Creates a vertical mirror image by reflecting the pixels around the central x-axis while rotating
 * them by 270 degrees
 * In-place form of {Image#transverse}.
 *
 * @return [Magick::Image] self
 * @see Image#transverse
 */
VALUE
Image_transverse_bang(VALUE self)
{
    rm_check_frozen(self);
    return crisscross(True, self, GVL_FUNC(TransverseImage));
}


/**
 * Convenient front-end to CropImage.
 *
 * No Ruby usage (internal function)
 *
 * Notes:
 *   - Respects fuzz attribute.
 *
 * @param bang whether the bang (!) version of the method was called
 * @param argc number of input arguments
 * @param argv array of input arguments
 * @param self this object
 * @return self if bang, otherwise a new image
 * @see Image_trim
 * @see Image_trim_bang
 */
static VALUE
trimmer(int bang, int argc, VALUE *argv, VALUE self)
{
    Image *image, *new_image;
    ExceptionInfo *exception;
    int reset_page = 0;

    switch (argc)
    {
        case 1:
            reset_page = RTEST(argv[0]);
        case 0:
            break;
        default:
            rb_raise(rb_eArgError, "wrong number of arguments (expecting 0 or 1, got %d)", argc);
            break;
    }

    TypedData_Get_Struct(self, Image, &rm_image_data_type, image);

    exception = AcquireExceptionInfo();
    GVL_STRUCT_TYPE(TrimImage) args = { image, exception };
    new_image = (Image *)CALL_FUNC_WITHOUT_GVL(GVL_FUNC(TrimImage), &args);
    rm_check_exception(exception, new_image, DestroyOnError);
    DestroyExceptionInfo(exception);

    rm_ensure_result(new_image);

    if (reset_page)
    {
        ResetImagePage(new_image, "0x0+0+0");
    }

    if (bang)
    {
        UPDATE_DATA_PTR(self, new_image);
        rm_image_destroy(image);
        return self;
    }

    return rm_image_new(new_image);
}


/**
 * Removes the edges that are exactly the same color as the corner pixels. Use the fuzz attribute to
 * make trim remove edges that are nearly the same color as the corner pixels.
 *
 * @overload trim(reset = false)
 *   @param reset [Boolean] The trim method retains the offset information in the cropped
 *     image. This may cause the image to appear to be surrounded by blank or black space when viewed
 *     with an external viewer. This only occurs when the image is saved in a format (such as GIF)
 *     that saves offset information. To reset the offset data, use true as the argument to trim.
 *   @return [Magick::Image] a new image
 *   @see Image#trim!
 */
VALUE
Image_trim(int argc, VALUE *argv, VALUE self)
{
    rm_check_destroyed(self);
    return trimmer(False, argc, argv, self);
}


/**
 * Removes the edges that are exactly the same color as the corner pixels. Use the fuzz attribute to
 * make trim remove edges that are nearly the same color as the corner pixels.
 *
 * @overload trim!(reset = false)
 *   @param reset [Boolean] The trim method retains the offset information in the cropped
 *     image. This may cause the image to appear to be surrounded by blank or black space when viewed
 *     with an external viewer. This only occurs when the image is saved in a format (such as GIF)
 *     that saves offset information. To reset the offset data, use true as the argument to trim.
 *   @return [Magick::Image] self
 *   @see Image#trim
 */
VALUE
Image_trim_bang(int argc, VALUE *argv, VALUE self)
{
    rm_check_frozen(self);
    return trimmer(True, argc, argv, self);
}


/**
 * Get the direction that the image gravitates within the composite.
 *
 * @return [Magick::GravityType] the image gravity
 */
VALUE Image_gravity(VALUE self)
{
    Image *image = rm_check_destroyed(self);
    return GravityType_find(image->gravity);
}


/**
 * Set the direction that the image gravitates within the composite.
 *
 * @param gravity [Magick::GravityType] the image gravity
 * @return [Magick::GravityType] the given value
 */
VALUE Image_gravity_eq(VALUE self, VALUE gravity)
{
    Image *image = rm_check_frozen(self);
    VALUE_TO_ENUM(gravity, image->gravity, GravityType);
    return gravity;
}


/**
 * Get the image type classification.
 * For example, GrayscaleType.
 * Don't confuse this attribute with the format, that is "GIF" or "JPG".
 *
 * @return [Magick::ImageType] the image type
 */
VALUE Image_image_type(VALUE self)
{
    Image *image;
    ImageType type;
#if defined(IMAGEMAGICK_6)
    ExceptionInfo *exception;
#endif

    image = rm_check_destroyed(self);
#if defined(IMAGEMAGICK_7)
    type = GetImageType(image);
#else
    exception = AcquireExceptionInfo();
    type = GetImageType(image, exception);
    CHECK_EXCEPTION();

    DestroyExceptionInfo(exception);
#endif

    return ImageType_find(type);
}


/**
 * Set the image type classification.
 *
 * @param image_type [Magick::ImageType] the image type
 * @return [Magick::ImageType] the given type
 */
VALUE Image_image_type_eq(VALUE self, VALUE image_type)
{
    Image *image;
    ImageType type;
#if defined(IMAGEMAGICK_7)
    ExceptionInfo *exception;
#endif

    image = rm_check_frozen(self);
    VALUE_TO_ENUM(image_type, type, ImageType);
#if defined(IMAGEMAGICK_7)
    exception = AcquireExceptionInfo();
    SetImageType(image, type, exception);
    CHECK_EXCEPTION();
    DestroyExceptionInfo(exception);
#else
    SetImageType(image, type);
#endif
    return image_type;
}


/**
 * Removes an artifact from the image and returns its value.
 *
 * @param artifact [String] the artifact
 * @return [Magick::Image] self
 * @see Image#define
 */
VALUE
Image_undefine(VALUE self, VALUE artifact)
{
    Image *image;
    char *key;

    image = rm_check_frozen(self);
    key = StringValueCStr(artifact);
    DeleteImageArtifact(image, key);
    return self;
}


/**
 * Constructs a new image with one pixel for each unique color in the image. The new image has 1
 * row. The row has 1 column for each unique pixel in the image.
 *
 * @return [Magick::Image] a new image
 */
VALUE
Image_unique_colors(VALUE self)
{
    Image *image, *new_image;
    ExceptionInfo *exception;

    image = rm_check_destroyed(self);
    exception = AcquireExceptionInfo();

    GVL_STRUCT_TYPE(UniqueImageColors) args = { image, exception };
    new_image = (Image *)CALL_FUNC_WITHOUT_GVL(GVL_FUNC(UniqueImageColors), &args);
    rm_check_exception(exception, new_image, DestroyOnError);
    DestroyExceptionInfo(exception);

    return rm_image_new(new_image);
}


/**
 * Get the units of image resolution.
 *
 * @return [Magick::ResolutionType] the resolution type
 */
VALUE
Image_units(VALUE self)
{
    Image *image = rm_check_destroyed(self);
    return ResolutionType_find(image->units);
}


/**
 * Set the units of image resolution.
 *
 * @param restype [Magick::ResolutionType] the resolution type
 * @return [Magick::ResolutionType] the given value
 */
VALUE
Image_units_eq(VALUE self, VALUE restype)
{
    ResolutionType units;
    Image *image = rm_check_frozen(self);

    VALUE_TO_ENUM(restype, units, ResolutionType);

    if (image->units != units)
    {
        switch (image->units)
        {
            case PixelsPerInchResolution:
                if (units == PixelsPerCentimeterResolution)
                {
#if defined(IMAGEMAGICK_7)
                    image->resolution.x /= 2.54;
                    image->resolution.y /= 2.54;
#else
                    image->x_resolution /= 2.54;
                    image->y_resolution /= 2.54;
#endif
                }
                break;

            case PixelsPerCentimeterResolution:
                if (units == PixelsPerInchResolution)
                {
#if defined(IMAGEMAGICK_7)
                    image->resolution.x *= 2.54;
                    image->resolution.y *= 2.54;
#else
                    image->x_resolution *= 2.54;
                    image->y_resolution *= 2.54;
#endif
                }
                break;

            default:
                // UndefinedResolution
#if defined(IMAGEMAGICK_7)
                image->resolution.x = 0.0;
                image->resolution.y = 0.0;
#else
                image->x_resolution = 0.0;
                image->y_resolution = 0.0;
#endif
                break;
        }

        image->units = units;
    }

    return restype;
}


/**
 * Sharpen an image. "amount" is the percentage of the difference between the original and the blur
 * image that is added back into the original. "threshold" is the threshold in pixels needed to
 * apply the diffence amount.
 *
 * No Ruby usage (internal function)
 *
 * @param argc number of input arguments
 * @param argv array of input arguments
 * @param radious the radious
 * @param sigma the sigma
 * @param amount the amount
 * @param threshold the threshold
 * @see Image_unsharp_mask
 */
static void
unsharp_mask_args(int argc, VALUE *argv, double *radius, double *sigma,
                  double *amount, double *threshold)
{
    switch (argc)
    {
        case 4:
            *threshold = NUM2DBL(argv[3]);
            if (*threshold < 0.0)
            {
                rb_raise(rb_eArgError, "threshold must be >= 0.0");
            }
        case 3:
            *amount = NUM2DBL(argv[2]);
            if (*amount <= 0.0)
            {
                rb_raise(rb_eArgError, "amount must be > 0.0");
            }
        case 2:
            *sigma = NUM2DBL(argv[1]);
            if (*sigma == 0.0)
            {
                rb_raise(rb_eArgError, "sigma must be != 0.0");
            }
        case 1:
            *radius = NUM2DBL(argv[0]);
            if (*radius < 0.0)
            {
                rb_raise(rb_eArgError, "radius must be >= 0.0");
            }
        case 0:
            break;

            // This case can't occur if we're called from Image_unsharp_mask_channel
            // because it has already raised an exception for the the argc > 4 case.
        default:
            rb_raise(rb_eArgError, "wrong number of arguments (%d for 0 to 4)", argc);
    }
}


/**
 * Sharpen an image. "amount" is the percentage of the difference between the original and the blur
 * image that is added back into the original. "threshold" is the threshold in pixels needed to
 * apply the diffence amount.
 *
 * @overload unsharp_mask(radius = 0.0, sigma = 1.0, amount = 1.0, threshold = 0.05)
 *   @param radius [Numeric] The radius of the Gaussian operator.
 *   @param sigma [Numeric] The standard deviation of the Gaussian operator.
 *   @param amount [Numeric] The percentage of the blurred image to be added to the receiver,
 *     specified as a fraction between 0 and 1.0
 *   @param threshold [Numeric] The threshold needed to apply the amount, specified as a fraction
 *     between 0 and 1.0
 *   @return [Magick::Image] a new image
 */
VALUE
Image_unsharp_mask(int argc, VALUE *argv, VALUE self)
{
    Image *image, *new_image;
    double radius = 0.0, sigma = 1.0, amount = 1.0, threshold = 0.05;
    ExceptionInfo *exception;

    image = rm_check_destroyed(self);

    unsharp_mask_args(argc, argv, &radius, &sigma, &amount, &threshold);

    exception = AcquireExceptionInfo();
    GVL_STRUCT_TYPE(UnsharpMaskImage) args = { image, radius, sigma, amount, threshold, exception };
    new_image = (Image *)CALL_FUNC_WITHOUT_GVL(GVL_FUNC(UnsharpMaskImage), &args);
    rm_check_exception(exception, new_image, DestroyOnError);
    DestroyExceptionInfo(exception);

    return rm_image_new(new_image);
}


/**
 * Sharpen an image. "amount" is the percentage of the difference between the original and the blur
 * image that is added back into the original. "threshold" is the threshold in pixels needed to
 * apply the diffence amount.
 *
 * Only the specified channels are sharpened.
 *
 * @overload unsharp_mask(radius = 0.0, sigma = 1.0, amount = 1.0, threshold = 0.05, channel = Magick::AllChannels)
 *   @param radius [Numeric] The radius of the Gaussian operator.
 *   @param sigma [Numeric] The standard deviation of the Gaussian operator.
 *   @param amount [Numeric] The percentage of the blurred image to be added to the receiver,
 *     specified as a fraction between 0 and 1.0
 *   @param threshold [Numeric] The threshold needed to apply the amount, specified as a fraction
 *     between 0 and 1.0
 *   @param channel [Magick::ChannelType] a ChannelType arguments.
 *
 * @overload unsharp_mask(radius = 0.0, sigma = 1.0, amount = 1.0, threshold = 0.05, *channels)
 *   @param radius [Numeric] The radius of the Gaussian operator.
 *   @param sigma [Numeric] The standard deviation of the Gaussian operator.
 *   @param amount [Numeric] The percentage of the blurred image to be added to the receiver,
 *     specified as a fraction between 0 and 1.0
 *   @param threshold [Numeric] The threshold needed to apply the amount, specified as a fraction
 *     between 0 and 1.0
 *   @param *channels [Magick::ChannelType] one or more ChannelType arguments.
 *
 * @return [Magick::Image] a new image
 */
VALUE
Image_unsharp_mask_channel(int argc, VALUE *argv, VALUE self)
{
    Image *image, *new_image;
    ChannelType channels;
    double radius = 0.0, sigma = 1.0, amount = 1.0, threshold = 0.05;
    ExceptionInfo *exception;

    image = rm_check_destroyed(self);
    channels = extract_channels(&argc, argv);
    if (argc > 4)
    {
        raise_ChannelType_error(argv[argc-1]);
    }

    unsharp_mask_args(argc, argv, &radius, &sigma, &amount, &threshold);

    exception = AcquireExceptionInfo();
#if defined(IMAGEMAGICK_7)
    BEGIN_CHANNEL_MASK(image, channels);
    GVL_STRUCT_TYPE(UnsharpMaskImage) args = { image, radius, sigma, amount, threshold, exception };
    new_image = (Image *)CALL_FUNC_WITHOUT_GVL(GVL_FUNC(UnsharpMaskImage), &args);
    CHANGE_RESULT_CHANNEL_MASK(new_image);
    END_CHANNEL_MASK(image);
#else
    GVL_STRUCT_TYPE(UnsharpMaskImageChannel) args = { image, channels, radius, sigma, amount, threshold, exception };
    new_image = (Image *)CALL_FUNC_WITHOUT_GVL(GVL_FUNC(UnsharpMaskImageChannel), &args);
#endif
    rm_check_exception(exception, new_image, DestroyOnError);
    DestroyExceptionInfo(exception);

    return rm_image_new(new_image);
}


/**
 * Soften the edges of an image.
 *
 * @overload vignette(horz_radius = self.columns*0.1+0.5, vert_radius = self.rows*0.1+0.5, radius = 0.0, sigma = 1.0)
 *   @param horz_radius [Numeric] Influences the amount of background color in the horizontal dimension.
 *   @param vert_radius [Numeric] Influences the amount of background color in the vertical dimension.
 *   @param radius [Numeric] Controls the amount of blurring.
 *   @param sigma [Numeric] Controls the amount of blurring.
 *   @return [Magick::Image] a new image
 */
VALUE
Image_vignette(int argc, VALUE *argv, VALUE self)
{
    Image *image, *new_image;
    long horz_radius, vert_radius;
    double radius = 0.0, sigma = 10.0;
    ExceptionInfo *exception;

    image = rm_check_destroyed(self);

    horz_radius = (long)(image->columns * 0.10 + 0.5);
    vert_radius = (long)(image->rows * 0.10 + 0.5);

    switch (argc)
    {
        case 4:
            sigma = NUM2DBL(argv[3]);
        case 3:
            radius = NUM2DBL(argv[2]);
        case 2:
            vert_radius = NUM2INT(argv[1]);
        case 1:
            horz_radius = NUM2INT(argv[0]);
        case 0:
            break;
        default:
            rb_raise(rb_eArgError, "wrong number of arguments (%d for 0 to 4)", argc);
            break;
    }

    exception = AcquireExceptionInfo();

    GVL_STRUCT_TYPE(VignetteImage) args = { image, radius, sigma, horz_radius, vert_radius, exception };
    new_image = (Image *)CALL_FUNC_WITHOUT_GVL(GVL_FUNC(VignetteImage), &args);
    rm_check_exception(exception, new_image, DestroyOnError);
    DestroyExceptionInfo(exception);

    return rm_image_new(new_image);
}


/**
 * Get the "virtual pixels" behave.
 * Virtual pixels are pixels that are outside the boundaries of the image.
 *
 * @return [Magick::VirtualPixelMethod] the VirtualPixelMethod
 */
VALUE
Image_virtual_pixel_method(VALUE self)
{
    Image *image;
    VirtualPixelMethod vpm;

    image = rm_check_destroyed(self);
    vpm = GetImageVirtualPixelMethod(image);
    return VirtualPixelMethod_find(vpm);
}


/**
 * Specify how "virtual pixels" behave.
 * Virtual pixels are pixels that are outside the boundaries of the image.
 *
 * @param method [Magick::VirtualPixelMethod] the VirtualPixelMethod
 * @return [Magick::VirtualPixelMethod] the given method
 */
VALUE
Image_virtual_pixel_method_eq(VALUE self, VALUE method)
{
    Image *image;
    VirtualPixelMethod vpm;
#if defined(IMAGEMAGICK_7)
    ExceptionInfo *exception;
#endif

    image = rm_check_frozen(self);
    VALUE_TO_ENUM(method, vpm, VirtualPixelMethod);
#if defined(IMAGEMAGICK_7)
    exception = AcquireExceptionInfo();
    SetImageVirtualPixelMethod(image, vpm, exception);
    CHECK_EXCEPTION();
    DestroyExceptionInfo(exception);
#else
    SetImageVirtualPixelMethod(image, vpm);
    rm_check_image_exception(image, RetainOnError);
#endif
    return method;
}


/**
 * Composites a watermark image on the target image using the Modulate composite operator. This
 * composite operation operates in the HSL colorspace and combines part of the lightness, part of
 * the saturation, and all of the hue of each pixel in the watermark with the corresponding pixel in
 * the target image
 *
 * @overload watermark(mark, brightness = 1.0, saturation = 1.0, x_off = 0, y_off = 0)
 *   @param mark [Magick::Image, Magick::ImageList] Either an imagelist or an image. If an
 *     imagelist, uses the current image.
 *   @param brightness [Numeric, String] The fraction of the lightness component of the watermark pixels to be
 *     composited onto the target image. Must be a non-negative number or a string in the form
 *     "NN%". If lightness is a number it is interpreted as a percentage. Both 0.25 and "25%" mean
 *     25%. The default is 100%.
 *   @param saturation [Numeric, String] The fraction of the saturation component of the watermark pixels to
 *     be composited onto the target image. Must be a non-negative number or a string in the form
 *     "NN%". If lightness is a number it is interpreted as a percentage. Both 0.25 and "25%" mean
 *     25%. The default is 100%.
 *   @param x_off [Numeric] The offset of the watermark, measured from the left-hand side of the
 *     target image.
 *   @param y_off [Numeri] The offset of the watermark, measured from the top of the target image.
 *
 * @overload watermark(mark, brightness, saturation, gravity, x_off = 0, y_off = 0)
 *   @param mark [Magick::Image, Magick::ImageList] Either an imagelist or an image. If an
 *     imagelist, uses the current image.
 *   @param brightness [Numeric, String] The fraction of the lightness component of the watermark pixels to be
 *     composited onto the target image. Must be a non-negative number or a string in the form
 *     "NN%". If lightness is a number it is interpreted as a percentage. Both 0.25 and "25%" mean
 *     25%. The default is 100%.
 *   @param saturation [Numeric, String] The fraction of the saturation component of the watermark pixels to
 *     be composited onto the target image. Must be a non-negative number or a string in the form
 *     "NN%". If lightness is a number it is interpreted as a percentage. Both 0.25 and "25%" mean
 *     25%. The default is 100%.
 *   @param gravity [Magick::GravityType] the gravity for offset. the offsets are measured from the
 *     NorthWest corner by default.
 *   @param x_off [Numeric] The offset of the watermark, measured from the left-hand side of the
 *     target image.
 *   @param y_off [Numeri] The offset of the watermark, measured from the top of the target image.
 *
 * @return [Magick::Image] a new image
 */
VALUE
Image_watermark(int argc, VALUE *argv, VALUE self)
{
    Image *image, *overlay, *new_image;
    double src_percent = 100.0, dst_percent = 100.0;
    long x_offset = 0L, y_offset = 0L;
    char geometry[20];
    VALUE ovly;
#if defined(IMAGEMAGICK_7)
    ExceptionInfo *exception;
#endif

    image = rm_check_destroyed(self);

    if (argc < 1)
    {
        rb_raise(rb_eArgError, "wrong number of arguments (%d for 1 to 6)", argc);
    }

    ovly = rm_cur_image(argv[0]);
    overlay = rm_check_destroyed(ovly);

    if (argc > 3)
    {
        get_composite_offsets(argc-3, &argv[3], image, overlay, &x_offset, &y_offset);
        // There must be 3 arguments left
        argc = 3;
    }

    switch (argc)
    {
        case 3:
            dst_percent = rm_percentage(argv[2], 1.0) * 100.0;
        case 2:
            src_percent = rm_percentage(argv[1], 1.0) * 100.0;
        case 1:
            break;
        default:
            rb_raise(rb_eArgError, "wrong number of arguments (%d for 1 to 6)", argc);
            break;
    }

    blend_geometry(geometry, sizeof(geometry), src_percent, dst_percent);
    CloneString(&overlay->geometry, geometry);
    SetImageArtifact(overlay, "compose:args", geometry);

    new_image = rm_clone_image(image);
#if defined(IMAGEMAGICK_7)
    exception = AcquireExceptionInfo();
    GVL_STRUCT_TYPE(CompositeImage) args = { new_image, overlay, ModulateCompositeOp, MagickTrue, x_offset, y_offset, exception };
    CALL_FUNC_WITHOUT_GVL(GVL_FUNC(CompositeImage), &args);
    rm_check_exception(exception, new_image, DestroyOnError);
    DestroyExceptionInfo(exception);
#else
    GVL_STRUCT_TYPE(CompositeImage) args = { new_image, ModulateCompositeOp, overlay, x_offset, y_offset };
    CALL_FUNC_WITHOUT_GVL(GVL_FUNC(CompositeImage), &args);

    rm_check_image_exception(new_image, DestroyOnError);
#endif

    RB_GC_GUARD(ovly);

    return rm_image_new(new_image);
}


/**
 * Create a "ripple" effect in the image by shifting the pixels vertically along a sine wave whose
 * amplitude and wavelength is specified by the given parameters.
 *
 * @overload wave(amplitude = 25.0, wavelength = 150.0)
 *   @param amplitude [Numeric] the amplitude
 *   @param wavelength [Numeric] the wave length
 *   @return [Magick::Image] a new image
 */
VALUE
Image_wave(int argc, VALUE *argv, VALUE self)
{
    Image *image, *new_image;
    double amplitude = 25.0, wavelength = 150.0;
    ExceptionInfo *exception;

    image = rm_check_destroyed(self);
    switch (argc)
    {
        case 2:
            wavelength = NUM2DBL(argv[1]);
        case 1:
            amplitude = NUM2DBL(argv[0]);
        case 0:
            break;
        default:
            rb_raise(rb_eArgError, "wrong number of arguments (%d for 0 to 2)", argc);
            break;
    }

    exception = AcquireExceptionInfo();
#if defined(IMAGEMAGICK_7)
    GVL_STRUCT_TYPE(WaveImage) args = { image, amplitude, wavelength, image->interpolate, exception };
#else
    GVL_STRUCT_TYPE(WaveImage) args = { image, amplitude, wavelength, exception };
#endif
    new_image = (Image *)CALL_FUNC_WITHOUT_GVL(GVL_FUNC(WaveImage), &args);
    rm_check_exception(exception, new_image, DestroyOnError);
    DestroyExceptionInfo(exception);

    return rm_image_new(new_image);
}


/**
 * Creates a "wet floor" reflection. The reflection is an inverted copy of the image that changes
 * from partially transparent to entirely transparent. By default only the bottom third of the image
 * appears in the reflection.
 *
 * @overload wet_floor(initial = 0.5, rate = 1.0)
 *   @param initial [Numeric] A value between 0.0 and 1.0 that specifies the initial percentage of
 *     transparency. Higher values cause the top of the reflection to be more transparent, lower
 *     values less transparent. The default is 0.5, which means that the top of the reflection is 50%
 *     transparent.
 *   @param rate [Numeric] A non-negative value that specifies how rapidly the reflection transitions
 *     from the initial level of transparency to entirely transparent. The default value is 1.0, which
 *     means that the transition occurs in 1/3 the image height. Values greater than 1.0 speed up the
 *     transition (the reflection will have fewer rows), values lower than 1.0 slow down the
 *     transition (the reflection will have more rows). A value of 0.0 means that the level of
 *     transparency will not change.
 *   @return [Magick::Image] a new image
 *   @see http://en.wikipedia.org/wiki/Wet_floor_effect
 */
VALUE
Image_wet_floor(int argc, VALUE *argv, VALUE self)
{
    Image *image, *reflection, *flip_image;
#if defined(IMAGEMAGICK_7)
    const Quantum *p;
    Quantum *q;
#else
    const PixelPacket *p;
    PixelPacket *q;
#endif
    RectangleInfo geometry;
    long x, y, max_rows;
    double initial = 0.5;
    double rate = 1.0;
    double opacity, step;
    const char *func;
    ExceptionInfo *exception;

    image = rm_check_destroyed(self);
    switch (argc)
    {
        case 2:
            rate = NUM2DBL(argv[1]);
        case 1:
            initial = NUM2DBL(argv[0]);
        case 0:
            break;
        default:
            rb_raise(rb_eArgError, "wrong number of arguments (%d for 0 to 2)", argc);
            break;
    }


    if (initial < 0.0 || initial > 1.0)
    {
        rb_raise(rb_eArgError, "Initial transparency must be in the range 0.0-1.0 (%g)", initial);
    }
    if (rate < 0.0)
    {
        rb_raise(rb_eArgError, "Transparency change rate must be >= 0.0 (%g)", rate);
    }

#if defined(IMAGEMAGICK_7)
    initial *= QuantumRange;
#else
    initial *= TransparentOpacity;
#endif

    // The number of rows in which to transition from the initial level of
    // transparency to complete transparency. rate == 0.0 -> no change.
    if (rate > 0.0)
    {
        max_rows = (long)((double)image->rows) / (3.0 * rate);
        max_rows = (long)min((unsigned long)max_rows, image->rows);
#if defined(IMAGEMAGICK_7)
        step =  (QuantumRange - initial) / max_rows;
#else
        step =  (TransparentOpacity - initial) / max_rows;
#endif
    }
    else
    {
        max_rows = (long)image->rows;
        step = 0.0;
    }


    exception = AcquireExceptionInfo();
    GVL_STRUCT_TYPE(FlipImage) args_FlipImage = { image, exception };
    flip_image = (Image *)CALL_FUNC_WITHOUT_GVL(GVL_FUNC(FlipImage), &args_FlipImage);
    CHECK_EXCEPTION();


    geometry.x = 0;
    geometry.y = 0;
    geometry.width = image->columns;
    geometry.height = max_rows;
    GVL_STRUCT_TYPE(CropImage) args_CropImage = { flip_image, &geometry, exception };
    reflection = (Image *)CALL_FUNC_WITHOUT_GVL(GVL_FUNC(CropImage), &args_CropImage);
    DestroyImage(flip_image);
    CHECK_EXCEPTION();


#if defined(IMAGEMAGICK_7)
    GVL_STRUCT_TYPE(SetImageStorageClass) args_SetImageStorageClass = { reflection, DirectClass, exception };
    CALL_FUNC_WITHOUT_GVL(GVL_FUNC(SetImageStorageClass), &args_SetImageStorageClass);
    rm_check_exception(exception, reflection, DestroyOnError);
    GVL_STRUCT_TYPE(SetImageAlphaChannel) args_SetImageAlphaChannel = { reflection, ActivateAlphaChannel, exception };
    CALL_FUNC_WITHOUT_GVL(GVL_FUNC(SetImageAlphaChannel), &args_SetImageAlphaChannel);
    rm_check_exception(exception, reflection, DestroyOnError);
#else
    GVL_STRUCT_TYPE(SetImageStorageClass) args_SetImageStorageClass = { reflection, DirectClass };
    CALL_FUNC_WITHOUT_GVL(GVL_FUNC(SetImageStorageClass), &args_SetImageStorageClass);
    rm_check_image_exception(reflection, DestroyOnError);


    reflection->matte = MagickTrue;
#endif
    opacity = initial;

    for (y = 0; y < max_rows; y++)
    {
#if defined(IMAGEMAGICK_7)
        if (opacity > QuantumRange)
        {
            opacity = QuantumRange;
        }
#else
        if (opacity > TransparentOpacity)
        {
            opacity = TransparentOpacity;
        }
#endif

        GVL_STRUCT_TYPE(GetVirtualPixels) args_GetVirtualPixels = { reflection, 0, y, image->columns, 1, exception };
        void *ret = CALL_FUNC_WITHOUT_GVL(GVL_FUNC(GetVirtualPixels), &args_GetVirtualPixels);
        p = reinterpret_cast<decltype(p)>(ret);
        rm_check_exception(exception, reflection, DestroyOnError);
        if (!p)
        {
            func = "AcquireImagePixels";
            goto error;
        }

        q = QueueAuthenticPixels(reflection, 0, y, image->columns, 1, exception);

        rm_check_exception(exception, reflection, DestroyOnError);
        if (!q)
        {
            func = "SetImagePixels";
            goto error;
        }

        for (x = 0; x < (long) image->columns; x++)
        {
            // Never make a pixel *less* transparent than it already is.
#if defined(IMAGEMAGICK_7)
            *q = *p;
            SetPixelAlpha(reflection, min(GetPixelAlpha(image, q), QuantumRange - (Quantum)opacity), q);

            p += GetPixelChannels(reflection);
            q += GetPixelChannels(reflection);
#else
            q[x] = p[x];
            q[x].opacity = max(q[x].opacity, (Quantum)opacity);
#endif
        }

        GVL_STRUCT_TYPE(SyncAuthenticPixels) args_SyncAuthenticPixels = { reflection, exception };
        CALL_FUNC_WITHOUT_GVL(GVL_FUNC(SyncAuthenticPixels), &args_SyncAuthenticPixels);
        rm_check_exception(exception, reflection, DestroyOnError);

        opacity += step;
    }


    DestroyExceptionInfo(exception);
    return rm_image_new(reflection);

    error:
    DestroyExceptionInfo(exception);
    DestroyImage(reflection);
    rb_raise(rb_eRuntimeError, "%s failed on row %lu", func, y);
    return(VALUE)0;
}


/**
 * Forces all pixels above the threshold into white while leaving all pixels below the threshold
 * unchanged.
 *
 * @overload white_threshold(red, green, blue, alpha: alpha)
 *  @param red [Numeric] the number for red channel
 *  @param green [Numeric] the number for green channel
 *  @param blue [Numeric] the number for blue channel
 *  @param alpha [Numeric] the number for alpha channel
 *  @return [Magick::Image] a new image
 *  @see Image#black_threshold
 */
VALUE
Image_white_threshold(int argc, VALUE *argv, VALUE self)
{
    return threshold_image(argc, argv, self, GVL_FUNC(WhiteThresholdImage));
}


/**
 * Copy the filename to the Info and to the Image. Add format prefix if necessary. This complicated
 * code is necessary to handle filenames like the kind Tempfile.new produces, which have an
 * "extension" in the form ".n", which confuses SetMagickInfo. So we don't use SetMagickInfo any
 * longer.
 *
 * No Ruby usage (internal function)
 *
 * @param info the Info
 * @param file the file
 */
void add_format_prefix(Info *info, VALUE file)
{
    char *filename;
    size_t filename_l;
    const MagickInfo *magick_info, *magick_info2;
    ExceptionInfo *exception;
    char magic[MaxTextExtent];
    size_t magic_l;
    size_t prefix_l;
    char *p;

    // Convert arg to string. If an exception occurs raise an error condition.
    if (rb_respond_to(file, rb_intern("path")))
    {
        FilePathStringValue(file);
    }
    file = rb_rescue(RESCUE_FUNC(rb_String), file, RESCUE_EXCEPTION_HANDLER_FUNC(file_arg_rescue), file);

    filename = rm_str2cstr(file, &filename_l);

    if (*info->magick == '\0')
    {
        memset(info->filename, 0, sizeof(info->filename));
        memcpy(info->filename, filename, (size_t)min(filename_l, MaxTextExtent-1));
        return;
    }

    // If the filename starts with a prefix, and it's a valid image format
    // prefix, then check for a conflict. If it's not a valid format prefix,
    // ignore it.
    p = (char *)memchr(filename, ':', (size_t)filename_l);
    if (p)
    {
        memset(magic, '\0', sizeof(magic));
        magic_l = p - filename;
        if (magic_l >= sizeof(magic)) {
            magic_l = sizeof(magic) - 1;
        }
        memcpy(magic, filename, magic_l);

        exception = AcquireExceptionInfo();
        magick_info = GetMagickInfo(magic, exception);
        CHECK_EXCEPTION();
        DestroyExceptionInfo(exception);

        if (magick_info && magick_info->magick_module)
        {
            // We have to compare the module names because some formats have
            // more than one name. JPG and JPEG, for example.
            exception = AcquireExceptionInfo();
            magick_info2 = GetMagickInfo(info->magick, exception);
            CHECK_EXCEPTION();
            DestroyExceptionInfo(exception);

            if (magick_info2->magick_module && strcmp(magick_info->magick_module, magick_info2->magick_module) != 0)
            {
                rb_raise(rb_eRuntimeError,
                         "filename prefix `%s' conflicts with output format `%s'",
                         magick_info->name, info->magick);
            }

            // The filename prefix already matches the specified format.
            // Just copy the filename as-is.
            memset(info->filename, 0, sizeof(info->filename));
            filename_l = min((size_t)filename_l, sizeof(info->filename));
            memcpy(info->filename, filename, (size_t)filename_l);
            return;
        }
    }

    // The filename doesn't start with a format prefix. Add the format from
    // the image info as the filename prefix.

    memset(info->filename, 0, sizeof(info->filename));
    prefix_l = min(sizeof(info->filename)-1, rm_strnlen_s(info->magick, sizeof(info->magick)));
    memcpy(info->filename, info->magick, prefix_l);
    info->filename[prefix_l++] = ':';

    filename_l = min(sizeof(info->filename) - prefix_l - 1, (size_t)filename_l);
    memcpy(info->filename+prefix_l, filename, (size_t)filename_l);
    info->filename[prefix_l+filename_l] = '\0';

    return;
}


/**
 * Write the image to the file.
 *
 * @param file [File, String] the file
 * @yield [info]
 * @yieldparam info [Magick::Image::Info]
 * @return [Magick::Image] self
 */
VALUE
Image_write(VALUE self, VALUE file)
{
    Image *image;
    Info *info;
    VALUE info_obj;
#if defined(IMAGEMAGICK_7)
    ExceptionInfo *exception;
#endif

    image = rm_check_destroyed(self);

    info_obj = rm_info_new();
    TypedData_Get_Struct(info_obj, Info, &rm_info_data_type, info);

    if (TYPE(file) == T_FILE)
    {
        rb_io_t *fptr;

        // Ensure file is open - raise error if not
        GetOpenFile(file, fptr);
        rb_io_check_writable(fptr);

        add_format_prefix(info, rm_io_path(file));
#if defined(_WIN32) && !defined(IMAGEMAGICK_GREATER_THAN_EQUAL_7_1_2)
        SetImageInfoFile(info, NULL);
#else
        SetImageInfoFile(info, rb_io_stdio_file(fptr));
#endif
    }
    else
    {
        add_format_prefix(info, file);
        SetImageInfoFile(info, NULL);
    }
    strlcpy(image->filename, info->filename, sizeof(image->filename));

    rm_sync_image_options(image, info);

    info->adjoin = MagickFalse;
#if defined(IMAGEMAGICK_7)
    exception = AcquireExceptionInfo();
    GVL_STRUCT_TYPE(WriteImage) args = { info, image, exception };
    CALL_FUNC_WITHOUT_GVL(GVL_FUNC(WriteImage), &args);
    CHECK_EXCEPTION();
    DestroyExceptionInfo(exception);
#else
    GVL_STRUCT_TYPE(WriteImage) args = { info, image };
    CALL_FUNC_WITHOUT_GVL(GVL_FUNC(WriteImage), &args);
    rm_check_image_exception(image, RetainOnError);
#endif

    RB_GC_GUARD(info_obj);

    return self;
}

#if defined(IMAGEMAGICK_7)
/**
 * Get the horizontal resolution of the image.
 *
 * @return [Float] the resolution
 */
VALUE
Image_x_resolution(VALUE self)
{
    IMPLEMENT_TYPED_ATTR_READERF(Image, x_resolution, resolution.x, dbl, &rm_image_data_type);
}

/**
 * Set the horizontal resolution of the image.
 *
 * @param val [Numeric] the resolution
 * @return [Numeric] the given resolution
 */
VALUE
Image_x_resolution_eq(VALUE self, VALUE val)
{
    IMPLEMENT_TYPED_ATTR_WRITERF(Image, x_resolution, resolution.x, dbl, &rm_image_data_type);
}

/**
 * Get the vertical resolution of the image.
 *
 * @return [Float] the resolution
 */
VALUE
Image_y_resolution(VALUE self)
{
    IMPLEMENT_TYPED_ATTR_READERF(Image, y_resolution, resolution.y, dbl, &rm_image_data_type);
}

/**
 * Set the vertical resolution of the image.
 *
 * @param val [Numeric] the resolution
 * @return [Numeric] the given resolution
 */
VALUE
Image_y_resolution_eq(VALUE self, VALUE val)
{
    IMPLEMENT_TYPED_ATTR_WRITERF(Image, y_resolution, resolution.y, dbl, &rm_image_data_type);
}
#else
/**
 * Get the horizontal resolution of the image.
 *
 * @return [Float] the resolution
 */
VALUE
Image_x_resolution(VALUE self)
{
    IMPLEMENT_TYPED_ATTR_READER(Image, x_resolution, dbl, &rm_image_data_type);
}

/**
 * Set the horizontal resolution of the image.
 *
 * @param val [Numeric] the resolution
 * @return [Numeric] the given resolution
 */
VALUE
Image_x_resolution_eq(VALUE self, VALUE val)
{
    IMPLEMENT_TYPED_ATTR_WRITER(Image, x_resolution, dbl, &rm_image_data_type);
}

/**
 * Get the vertical resolution of the image.
 *
 * @return [Float] the resolution
 */
VALUE
Image_y_resolution(VALUE self)
{
    IMPLEMENT_TYPED_ATTR_READER(Image, y_resolution, dbl, &rm_image_data_type);
}

/**
 * Set the vertical resolution of the image.
 *
 * @param val [Numeric] the resolution
 * @return [Numeric] the given resolution
 */
VALUE
Image_y_resolution_eq(VALUE self, VALUE val)
{
    IMPLEMENT_TYPED_ATTR_WRITER(Image, y_resolution, dbl, &rm_image_data_type);
}
#endif


/**
 * Determine if the argument list is x, y, width, height
 * or
 * gravity, width, height
 * or
 * gravity, x, y, width, height
 *
 * If the 2nd or 3rd, compute new x, y values.
 *
 * The argument list can have a trailing true, false, or nil argument. If
 * present and true, after cropping reset the page fields in the image.
 *
 * No Ruby usage (internal function)
 *
 * Notes:
 *   - Call xform_image to do the cropping.
 *
 * @param bang whether the bang (!) version of the method was called
 * @param argc number of input arguments
 * @param argv array of input arguments
 * @param self this object
 * @return self if bang, otherwise a new image
 * @see xform_image
 */
static VALUE
cropper(int bang, int argc, VALUE *argv, VALUE self)
{
    VALUE x, y, width, height;
    unsigned long nx = 0, ny = 0;
    unsigned long columns, rows;
    int reset_page = 0;
    GravityType gravity;
    Image *image;
    VALUE cropped;

    // Check for a "reset page" trailing argument.
    if (argc >= 1)
    {
        switch (TYPE(argv[argc-1]))
        {
            case T_TRUE:
                reset_page = 1;
                // fall thru
            case T_FALSE:
            case T_NIL:
                argc -= 1;
            default:
                break;
        }
    }

    switch (argc)
    {
        case 5:
            TypedData_Get_Struct(self, Image, &rm_image_data_type, image);

            VALUE_TO_ENUM(argv[0], gravity, GravityType);

            x      = argv[1];
            y      = argv[2];
            width  = argv[3];
            height = argv[4];

            nx      = NUM2ULONG(x);
            ny      = NUM2ULONG(y);
            columns = NUM2ULONG(width);
            rows    = NUM2ULONG(height);

            switch (gravity)
            {
                case NorthEastGravity:
                case EastGravity:
                case SouthEastGravity:
                    nx = image->columns - columns - nx;
                    break;
                case NorthGravity:
                case SouthGravity:
                case CenterGravity:
                    nx += image->columns/2 - columns/2;
                    break;
                default:
                    break;
            }
            switch (gravity)
            {
                case SouthWestGravity:
                case SouthGravity:
                case SouthEastGravity:
                    ny = image->rows - rows - ny;
                    break;
                case EastGravity:
                case WestGravity:
                case CenterGravity:
                    ny += image->rows/2 - rows/2;
                    break;
                case NorthEastGravity:
                case NorthGravity:
                default:
                    break;
            }

            x = ULONG2NUM(nx);
            y = ULONG2NUM(ny);
            break;
        case 4:
            x      = argv[0];
            y      = argv[1];
            width  = argv[2];
            height = argv[3];
            break;
        case 3:

            // Convert the width & height arguments to unsigned longs.
            // Compute the x & y offsets from the gravity and then
            // convert them to VALUEs.
            VALUE_TO_ENUM(argv[0], gravity, GravityType);
            width   = argv[1];
            height  = argv[2];
            columns = NUM2ULONG(width);
            rows    = NUM2ULONG(height);

            TypedData_Get_Struct(self, Image, &rm_image_data_type, image);

            switch (gravity)
            {
                case ForgetGravity:
                case NorthWestGravity:
                    nx = 0;
                    ny = 0;
                    break;
                case NorthGravity:
                    nx = (image->columns - columns) / 2;
                    ny = 0;
                    break;
                case NorthEastGravity:
                    nx = image->columns - columns;
                    ny = 0;
                    break;
                case WestGravity:
                    nx = 0;
                    ny = (image->rows - rows) / 2;
                    break;
                case EastGravity:
                    nx = image->columns - columns;
                    ny = (image->rows - rows) / 2;
                    break;
                case SouthWestGravity:
                    nx = 0;
                    ny = image->rows - rows;
                    break;
                case SouthGravity:
                    nx = (image->columns - columns) / 2;
                    ny = image->rows - rows;
                    break;
                case SouthEastGravity:
                    nx = image->columns - columns;
                    ny = image->rows - rows;
                    break;
                case CenterGravity:
                    nx = (image->columns - columns) / 2;
                    ny = (image->rows - rows) / 2;
                    break;
#if defined(IMAGEMAGICK_6)
                case StaticGravity:
                    rb_raise(rb_eNotImpError, "`StaticGravity' is not supported");
                    break;
#endif
            }

            x = ULONG2NUM(nx);
            y = ULONG2NUM(ny);
            break;
        default:
            if (reset_page)
            {
                rb_raise(rb_eArgError, "wrong number of arguments (%d for 4, 5, or 6)", argc);
            }
            else
            {
                rb_raise(rb_eArgError, "wrong number of arguments (%d for 3, 4, or 5)", argc);
            }
            break;
    }

    cropped = xform_image(bang, self, x, y, width, height, GVL_FUNC(CropImage));
    if (reset_page)
    {
        TypedData_Get_Struct(cropped, Image, &rm_image_data_type, image);
        ResetImagePage(image, "0x0+0+0");
    }

    RB_GC_GUARD(x);
    RB_GC_GUARD(y);
    RB_GC_GUARD(width);
    RB_GC_GUARD(height);

    return cropped;
}


// aliases for common use of structure types; ChopImage, CropImage, ShaveImage
typedef GVL_STRUCT_TYPE(ChopImage) GVL_STRUCT_TYPE(xform_image);

/**
 * Call one of the image transformation functions.
 *
 * No Ruby usage (internal function)
 *
 * @param bang whether the bang (!) version of the method was called
 * @param self this object
 * @param x x position of start of region
 * @param y y position of start of region
 * @param width width of region
 * @param height height of region
 * @param xformer the transformation function
 * @return self if bang, otherwise a new image
 */
static VALUE
xform_image(int bang, VALUE self, VALUE x, VALUE y, VALUE width, VALUE height, gvl_function_t fp)
{
    Image *image, *new_image;
    RectangleInfo rect;
    ExceptionInfo *exception;

    TypedData_Get_Struct(self, Image, &rm_image_data_type, image);
    rect.x      = NUM2LONG(x);
    rect.y      = NUM2LONG(y);
    rect.width  = NUM2ULONG(width);
    rect.height = NUM2ULONG(height);

    exception = AcquireExceptionInfo();

    GVL_STRUCT_TYPE(xform_image) args = { image, &rect, exception };
    new_image = (Image *)CALL_FUNC_WITHOUT_GVL(fp, &args);

    // An exception can occur in either the old or the new images
    rm_check_exception(exception, new_image, DestroyOnError);
    DestroyExceptionInfo(exception);

#if defined(IMAGEMAGICK_6)
    if (rm_should_raise_exception(&image->exception, RetainExceptionRetention))
    {
        DestroyImage(new_image);
        rm_check_image_exception(image, RetainOnError);
    }
#endif

    if (bang)
    {
        rm_ensure_result(new_image);
        UPDATE_DATA_PTR(self, new_image);
        rm_image_destroy(image);
        return self;
    }

    return rm_image_new(new_image);

}


/**
 * Remove all the ChannelType arguments from the end of the argument list.
 *
 * No Ruby usage (internal function)
 *
 * Notes:
 *   - Returns DefaultChannels if no channel arguments were found.
 *   - Returns the number of remaining arguments.
 *
 * @param argc number of input arguments
 * @param argv array of input arguments
 * @return A ChannelType value suitable for passing into an xMagick function.
 */
ChannelType extract_channels(int *argc, VALUE *argv)
{
    VALUE arg;
    ChannelType channels, ch_arg;

    channels = UndefinedChannel;
    while (*argc > 0)
    {
        arg = argv[(*argc)-1];

        // Stop when you find a non-ChannelType argument
        if (CLASS_OF(arg) != Class_ChannelType)
        {
            break;
        }
        VALUE_TO_ENUM(arg, ch_arg, ChannelType);
        channels = (ChannelType)(channels | ch_arg);
        *argc -= 1;
    }

    if (channels == 0)
    {
        channels = DefaultChannels;
    }

    RB_GC_GUARD(arg);

    return channels;
}


/**
 * Raise TypeError when an non-ChannelType object is unexpectedly encountered.
 *
 * No Ruby usage (internal function)
 *
 * @param arg the argument
 */
void
raise_ChannelType_error(VALUE arg)
{
    rb_raise(rb_eTypeError, "argument must be a ChannelType value (%s given)",
             rb_class2name(CLASS_OF(arg)));
}


/**
 * Destroy an image. Called from GC when all references to the image have gone
 * out of scope.
 *
 * No Ruby usage (internal function)
 *
 * Notes:
 *   - A NULL Image pointer indicates that the image has already been destroyed
 *     by Image#destroy!
 *
 * @param img the image
 */
void rm_image_destroy(void *img)
{
    Image *image = (Image *)img;

    if (img != NULL)
    {
        DestroyImage(image);
    }
}

/**
  * Get Image object size.
  *
  * No Ruby usage (internal function)
  *
  * @param ptr pointer to the Image object
  */
static size_t
rm_image_memsize(const void *ptr)
{
    return sizeof(Image);
}
