Commit eb212cfd6 for imagemagick.org

commit eb212cfd6fd667c04da2f6796ef7974498f35ba0
Author: Greg B <64932474+gregbenz@users.noreply.github.com>
Date:   Wed Jun 10 17:57:18 2026 -0500

    Preserve Ultra HDR JPEG gain maps when transcoding (#8788)

    Co-authored-by: Greg Benz <git@gregbenzphotography.com>

diff --git a/MagickCore/profile-private.h b/MagickCore/profile-private.h
index 56def58d1..96c7e56b4 100644
--- a/MagickCore/profile-private.h
+++ b/MagickCore/profile-private.h
@@ -33,6 +33,8 @@ extern MagickExport StringInfo
     ExceptionInfo *exception);

 extern MagickPrivate void
+  AppendImageProfileProperty(Image *,const char *,const char *,const char *,
+    ExceptionInfo *),
   Update8BIMClipPath(const Image *,const size_t,const size_t,
     const RectangleInfo *),
   SyncImageProfiles(Image *);
diff --git a/MagickCore/profile.c b/MagickCore/profile.c
index d12396905..c1edb51d4 100644
--- a/MagickCore/profile.c
+++ b/MagickCore/profile.c
@@ -91,6 +91,67 @@ static MagickBooleanType
 static void
   WriteTo8BimProfile(Image *,const char*,const StringInfo *);

+/*
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%                                                                             %
+%                                                                             %
+%                                                                             %
+%   A p p e n d I m a g e P r o f i l e P r o p e r t y                       %
+%                                                                             %
+%                                                                             %
+%                                                                             %
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%
+%  AppendImageProfileProperty() appends a semicolon-delimited value to an image
+%  property when the image contains the requested profile.
+%
+%  The format of the AppendImageProfileProperty method is:
+%
+%      void AppendImageProfileProperty(Image *image,const char *profile,
+%        const char *property,const char *value,ExceptionInfo *exception)
+%
+%  A description of each parameter follows:
+%
+%    o image: the image.
+%
+%    o profile: the image profile name.
+%
+%    o property: the image property name.
+%
+%    o value: the property value to append.
+%
+%    o exception: return any errors or warnings in this structure.
+%
+*/
+MagickPrivate void AppendImageProfileProperty(Image *image,const char *profile,
+  const char *property,const char *value,ExceptionInfo *exception)
+{
+  char
+    *property_value;
+
+  const char
+    *current;
+
+  assert(image != (Image *) NULL);
+  assert(image->signature == MagickCoreSignature);
+  assert(profile != (const char *) NULL);
+  assert(property != (const char *) NULL);
+  assert(value != (const char *) NULL);
+  if (GetImageProfile(image,profile) == (const StringInfo *) NULL)
+    return;
+  current=GetImageProperty(image,property,exception);
+  if ((current == (const char *) NULL) || (*current == '\0'))
+    {
+      (void) SetImageProperty(image,property,value,exception);
+      return;
+    }
+  property_value=AcquireString(current);
+  (void) ConcatenateString(&property_value,";");
+  (void) ConcatenateString(&property_value,value);
+  (void) SetImageProperty(image,property,property_value,exception);
+  property_value=DestroyString(property_value);
+}
+
 /*
   Typedef declarations
 */
diff --git a/MagickCore/resize.c b/MagickCore/resize.c
index 50fa64ab6..551d90fa2 100644
--- a/MagickCore/resize.c
+++ b/MagickCore/resize.c
@@ -68,6 +68,7 @@
 #include "MagickCore/nt-base-private.h"
 #include "MagickCore/option.h"
 #include "MagickCore/pixel.h"
+#include "MagickCore/profile-private.h"
 #include "MagickCore/quantum-private.h"
 #include "MagickCore/resample.h"
 #include "MagickCore/resample-private.h"
@@ -3873,6 +3874,17 @@ MagickExport Image *ResizeImage(const Image *image,const size_t columns,
       return((Image *) NULL);
     }
   resize_image->type=image->type;
+  {
+    char
+      transform[MagickPathExtent];
+
+    (void) FormatLocaleString(transform,MagickPathExtent,
+      "resize %.20gx%.20g %.20gx%.20g",(double) image->columns,
+      (double) image->rows,(double) resize_image->columns,
+      (double) resize_image->rows);
+    AppendImageProfileProperty(resize_image,"hdrgm","hdrgm:Transform",
+      transform,exception);
+  }
   return(resize_image);
 }

diff --git a/MagickCore/shear.c b/MagickCore/shear.c
index 62eef2e21..b54650bb1 100644
--- a/MagickCore/shear.c
+++ b/MagickCore/shear.c
@@ -70,6 +70,7 @@
 #include "MagickCore/monitor-private.h"
 #include "MagickCore/nt-base-private.h"
 #include "MagickCore/pixel-accessor.h"
+#include "MagickCore/profile-private.h"
 #include "MagickCore/quantum.h"
 #include "MagickCore/resource_.h"
 #include "MagickCore/shear.h"
@@ -85,7 +86,7 @@
 %                                                                             %
 %                                                                             %
 %                                                                             %
-+   C r o p T o F i t I m a g e                                               %
+%   C r o p T o F i t I m a g e                                               %
 %                                                                             %
 %                                                                             %
 %                                                                             %
@@ -1090,6 +1091,17 @@ MagickExport Image *IntegralRotateImage(const Image *image,size_t rotations,
   image_view=DestroyCacheView(image_view);
   rotate_image->type=image->type;
   rotate_image->page=page;
+  if (status != MagickFalse)
+    {
+      char
+        transform[MagickPathExtent];
+
+      (void) FormatLocaleString(transform,MagickPathExtent,
+        "rotate %.20gx%.20g %.20g",(double) image->columns,
+        (double) image->rows,(double) rotations);
+      AppendImageProfileProperty(rotate_image,"hdrgm","hdrgm:Transform",
+        transform,exception);
+    }
   if (status == MagickFalse)
     rotate_image=DestroyImage(rotate_image);
   return(rotate_image);
diff --git a/MagickCore/transform.c b/MagickCore/transform.c
index 5931cc9bf..4f23a1ac8 100644
--- a/MagickCore/transform.c
+++ b/MagickCore/transform.c
@@ -742,6 +742,18 @@ MagickExport Image *CropImage(const Image *image,const RectangleInfo *geometry,
   crop_view=DestroyCacheView(crop_view);
   image_view=DestroyCacheView(image_view);
   crop_image->type=image->type;
+  if (status != MagickFalse)
+    {
+      char
+        transform[MagickPathExtent];
+
+      (void) FormatLocaleString(transform,MagickPathExtent,
+        "crop %.20gx%.20g %.20gx%.20g%+.20g%+.20g",
+        (double) image->columns,(double) image->rows,(double) page.width,
+        (double) page.height,(double) page.x,(double) page.y);
+      AppendImageProfileProperty(crop_image,"hdrgm","hdrgm:Transform",
+        transform,exception);
+    }
   if (status == MagickFalse)
     crop_image=DestroyImage(crop_image);
   return(crop_image);
@@ -1293,6 +1305,16 @@ MagickExport Image *FlipImage(const Image *image,ExceptionInfo *exception)
   if (page.height != 0)
     page.y=((ssize_t) page.height-(ssize_t) flip_image->rows-page.y);
   flip_image->page=page;
+  if (status != MagickFalse)
+    {
+      char
+        transform[MagickPathExtent];
+
+      (void) FormatLocaleString(transform,MagickPathExtent,
+        "flip %.20gx%.20g",(double) image->columns,(double) image->rows);
+      AppendImageProfileProperty(flip_image,"hdrgm","hdrgm:Transform",
+        transform,exception);
+    }
   if (status == MagickFalse)
     flip_image=DestroyImage(flip_image);
   return(flip_image);
@@ -1429,6 +1451,16 @@ MagickExport Image *FlopImage(const Image *image,ExceptionInfo *exception)
   if (page.width != 0)
     page.x=((ssize_t) page.width-(ssize_t) flop_image->columns-page.x);
   flop_image->page=page;
+  if (status != MagickFalse)
+    {
+      char
+        transform[MagickPathExtent];
+
+      (void) FormatLocaleString(transform,MagickPathExtent,
+        "flop %.20gx%.20g",(double) image->columns,(double) image->rows);
+      AppendImageProfileProperty(flop_image,"hdrgm","hdrgm:Transform",
+        transform,exception);
+    }
   if (status == MagickFalse)
     flop_image=DestroyImage(flop_image);
   return(flop_image);
@@ -2231,6 +2263,17 @@ MagickExport Image *TransposeImage(const Image *image,ExceptionInfo *exception)
   Swap(page.width,page.height);
   Swap(page.x,page.y);
   transpose_image->page=page;
+  if (status != MagickFalse)
+    {
+      char
+        transform[MagickPathExtent];
+
+      (void) FormatLocaleString(transform,MagickPathExtent,
+        "transpose %.20gx%.20g",(double) image->columns,
+        (double) image->rows);
+      AppendImageProfileProperty(transpose_image,"hdrgm","hdrgm:Transform",
+        transform,exception);
+    }
   if (status == MagickFalse)
     transpose_image=DestroyImage(transpose_image);
   return(transpose_image);
@@ -2377,6 +2420,17 @@ MagickExport Image *TransverseImage(const Image *image,ExceptionInfo *exception)
   if (page.height != 0)
     page.y=(ssize_t) page.height-(ssize_t) transverse_image->rows-page.y;
   transverse_image->page=page;
+  if (status != MagickFalse)
+    {
+      char
+        transform[MagickPathExtent];
+
+      (void) FormatLocaleString(transform,MagickPathExtent,
+        "transverse %.20gx%.20g",(double) image->columns,
+        (double) image->rows);
+      AppendImageProfileProperty(transverse_image,"hdrgm","hdrgm:Transform",
+        transform,exception);
+    }
   if (status == MagickFalse)
     transverse_image=DestroyImage(transverse_image);
   return(transverse_image);
diff --git a/coders/uhdr.c b/coders/uhdr.c
index 01d03ba3e..32cceb3f2 100644
--- a/coders/uhdr.c
+++ b/coders/uhdr.c
@@ -42,9 +42,13 @@
 #include "MagickCore/list.h"
 #include "MagickCore/module.h"
 #include "MagickCore/option.h"
+#include "MagickCore/profile.h"
 #include "MagickCore/profile-private.h"
+#include "MagickCore/resize.h"
+#include "MagickCore/shear.h"
 #include "MagickCore/string_.h"
 #include "MagickCore/string-private.h"
+#include "MagickCore/transform.h"
 #if defined(MAGICKCORE_UHDR_DELEGATE)
 #include "ultrahdr_api.h"
 #include "uhdr.h"
@@ -146,6 +150,9 @@ static Image *ReadUHDRImage(const ImageInfo *image_info,
 #define SetHDRGMPropertyInt(name,value) \
   (void) FormatLocaleString(buffer,sizeof(buffer),"%d",(value)); \
   (void) SetImageProperty(image,"hdrgm:" name,buffer,exception)
+#define SetHDRGMPropertySize(name,value) \
+  (void) FormatLocaleString(buffer,sizeof(buffer),"%.20g",(double) (value)); \
+  (void) SetImageProperty(image,"hdrgm:" name,buffer,exception)
 #define SetHDRGMProperty3(name,value,value1,value2) \
   (void) FormatLocaleString(buffer,sizeof(buffer),"%f,%f,%f", \
      (value),(value1),(value2)); \
@@ -266,6 +273,8 @@ static Image *ReadUHDRImage(const ImageInfo *image_info,
       SetHDRGMProperty("HDRCapacityMin",gainmap_info->hdr_capacity_min);
       SetHDRGMProperty("HDRCapacityMax",gainmap_info->hdr_capacity_max);
       SetHDRGMPropertyInt("UseBaseColorGrade",gainmap_info->use_base_cg);
+      SetHDRGMPropertySize("BaseWidth",image->columns);
+      SetHDRGMPropertySize("BaseHeight",image->rows);
     }
   }

@@ -287,6 +296,30 @@ static Image *ReadUHDRImage(const ImageInfo *image_info,
     (void) SetImageProfilePrivate(image,exif_data,exception);
   }

+  uhdr_mem_block_t *icc = uhdr_dec_get_icc(handle);
+  if ((icc != NULL) && (icc->data != NULL) && (icc->data_sz != 0))
+  {
+    const unsigned char
+      *icc_data_start = (const unsigned char *) icc->data;
+
+    size_t
+      icc_data_size = icc->data_sz;
+
+    /*
+      libultrahdr returns JPEG APP2 ICC chunk data. ImageMagick stores the
+      raw ICC payload and adds the APP2 wrapper itself when writing JPEG.
+    */
+    if ((icc_data_size > 14) &&
+        (memcmp(icc_data_start,"ICC_PROFILE\0",12) == 0))
+      {
+        icc_data_start+=14;
+        icc_data_size-=14;
+      }
+    StringInfo *icc_data = BlobToProfileStringInfo("icc",icc_data_start,
+      icc_data_size,exception);
+    (void) SetImageProfilePrivate(image,icc_data,exception);
+  }
+
   SetImageColorspace(image, RGBColorspace, exception);

   if (decoded_img_ct == UHDR_CT_LINEAR)
@@ -620,6 +653,471 @@ static void fillRawImageDescriptor(uhdr_raw_image_t *imgDescriptor, const ImageI
   imgDescriptor->h = image->rows;
 }

+static size_t GetHDRGMPropertySize(const Image *image,const char *name,
+  ExceptionInfo *exception)
+{
+  char
+    property[MagickPathExtent];
+
+  const char
+    *value;
+
+  (void) FormatLocaleString(property,MagickPathExtent,"hdrgm:%s",name);
+  value=GetImageProperty(image,property,exception);
+  if (value == (const char *) NULL)
+    return(0);
+  return((size_t) StringToUnsignedLong(value));
+}
+
+static size_t ScaleGainMapExtent(const size_t extent,
+  const size_t scaled_extent,const size_t base_extent)
+{
+  double
+    scale;
+
+  if ((extent == 0) || (scaled_extent == 0) || (base_extent == 0))
+    return(0);
+  scale=((double) extent*(double) scaled_extent)/(double) base_extent;
+  if (scale < 1.0)
+    return(1);
+  if (scale > (double) MAGICK_SSIZE_MAX)
+    return(0);
+  return(CastDoubleToSizeT(scale+0.5));
+}
+
+static ssize_t ScaleGainMapCoordinate(const double coordinate,
+  const size_t extent,const size_t base_extent)
+{
+  double
+    scale;
+
+  if ((extent == 0) || (base_extent == 0))
+    return(0);
+  scale=((double) extent*coordinate)/(double) base_extent;
+  if (scale < 0.0)
+    return(0);
+  if (scale > (double) MAGICK_SSIZE_MAX)
+    return(MAGICK_SSIZE_MAX);
+  return(CastDoubleToSsizeT(floor(scale+0.5)));
+}
+
+static MagickBooleanType IsGainMapBaseGeometry(const size_t base_columns,
+  const size_t base_rows,const double columns,const double rows)
+{
+  if ((columns <= 0.0) || (rows <= 0.0))
+    return(MagickFalse);
+  if ((base_columns != CastDoubleToSizeT(columns)) ||
+      (base_rows != CastDoubleToSizeT(rows)))
+    return(MagickFalse);
+  return(MagickTrue);
+}
+
+static MagickBooleanType ReplaceGainMapImage(Image **gainmap_image,
+  Image *transform_image)
+{
+  if (transform_image == (Image *) NULL)
+    return(MagickFalse);
+  *gainmap_image=DestroyImageList(*gainmap_image);
+  *gainmap_image=transform_image;
+  return(MagickTrue);
+}
+
+static MagickBooleanType CropGainMapImage(Image **gainmap_image,
+  const size_t base_columns,const size_t base_rows,const double columns,
+  const double rows,const double x,const double y,ExceptionInfo *exception)
+{
+  Image
+    *crop_image;
+
+  RectangleInfo
+    geometry;
+
+  ssize_t
+    x0,
+    x1,
+    y0,
+    y1;
+
+  if ((base_columns == 0) || (base_rows == 0) || (columns <= 0.0) ||
+      (rows <= 0.0))
+    return(MagickFalse);
+  if ((x < 0.0) || (y < 0.0) || ((x+columns) > (double) base_columns) ||
+      ((y+rows) > (double) base_rows))
+    return(MagickFalse);
+  x0=ScaleGainMapCoordinate(x,(*gainmap_image)->columns,base_columns);
+  y0=ScaleGainMapCoordinate(y,(*gainmap_image)->rows,base_rows);
+  x1=ScaleGainMapCoordinate(x+columns,(*gainmap_image)->columns,base_columns);
+  y1=ScaleGainMapCoordinate(y+rows,(*gainmap_image)->rows,base_rows);
+  if (x0 >= (ssize_t) (*gainmap_image)->columns)
+    x0=(ssize_t) (*gainmap_image)->columns-1;
+  if (y0 >= (ssize_t) (*gainmap_image)->rows)
+    y0=(ssize_t) (*gainmap_image)->rows-1;
+  if (x1 > (ssize_t) (*gainmap_image)->columns)
+    x1=(ssize_t) (*gainmap_image)->columns;
+  if (y1 > (ssize_t) (*gainmap_image)->rows)
+    y1=(ssize_t) (*gainmap_image)->rows;
+  if (x1 <= x0)
+    x1=x0+1;
+  if (y1 <= y0)
+    y1=y0+1;
+  geometry.x=x0;
+  geometry.y=y0;
+  geometry.width=(size_t) (x1-x0);
+  geometry.height=(size_t) (y1-y0);
+  crop_image=CropImage(*gainmap_image,&geometry,exception);
+  return(ReplaceGainMapImage(gainmap_image,crop_image));
+}
+
+static MagickBooleanType ResizeGainMapImage(Image **gainmap_image,
+  size_t *base_columns,size_t *base_rows,const size_t columns,
+  const size_t rows,const Image *image,ExceptionInfo *exception)
+{
+  Image
+    *resize_image;
+
+  size_t
+    target_columns,
+    target_rows;
+
+  target_columns=ScaleGainMapExtent((*gainmap_image)->columns,columns,
+    *base_columns);
+  target_rows=ScaleGainMapExtent((*gainmap_image)->rows,rows,*base_rows);
+  if ((target_columns == 0) || (target_rows == 0))
+    return(MagickFalse);
+  if ((target_columns != (*gainmap_image)->columns) ||
+      (target_rows != (*gainmap_image)->rows))
+    {
+      resize_image=ResizeImage(*gainmap_image,target_columns,target_rows,
+        image->filter,exception);
+      if (ReplaceGainMapImage(gainmap_image,resize_image) == MagickFalse)
+        return(MagickFalse);
+    }
+  *base_columns=columns;
+  *base_rows=rows;
+  return(MagickTrue);
+}
+
+static MagickBooleanType ApplyGainMapTransform(Image **gainmap_image,
+  size_t *base_columns,size_t *base_rows,const Image *image,
+  const char *transform,ExceptionInfo *exception)
+{
+  double
+    columns,
+    rows,
+    source_columns,
+    source_rows,
+    x,
+    y;
+
+  Image
+    *transform_image;
+
+  size_t
+    rotations;
+
+  if (LocaleNCompare(transform,"crop ",5) == 0)
+    {
+      if (sscanf(transform+5,"%lfx%lf %lfx%lf%lf%lf",&source_columns,
+          &source_rows,&columns,&rows,&x,&y) != 6)
+        return(MagickFalse);
+      if (IsGainMapBaseGeometry(*base_columns,*base_rows,source_columns,
+          source_rows) == MagickFalse)
+        return(MagickFalse);
+      if (CropGainMapImage(gainmap_image,*base_columns,*base_rows,columns,
+          rows,x,y,exception) == MagickFalse)
+        return(MagickFalse);
+      *base_columns=CastDoubleToSizeT(columns);
+      *base_rows=CastDoubleToSizeT(rows);
+      return(MagickTrue);
+    }
+  if (LocaleNCompare(transform,"resize ",7) == 0)
+    {
+      if (sscanf(transform+7,"%lfx%lf %lfx%lf",&source_columns,
+          &source_rows,&columns,&rows) != 4)
+        return(MagickFalse);
+      if (IsGainMapBaseGeometry(*base_columns,*base_rows,source_columns,
+          source_rows) == MagickFalse)
+        return(MagickFalse);
+      return(ResizeGainMapImage(gainmap_image,base_columns,base_rows,
+        CastDoubleToSizeT(columns),CastDoubleToSizeT(rows),image,exception));
+    }
+  if (LocaleNCompare(transform,"flip ",5) == 0)
+    {
+      if (sscanf(transform+5,"%lfx%lf",&source_columns,&source_rows) != 2)
+        return(MagickFalse);
+      if (IsGainMapBaseGeometry(*base_columns,*base_rows,source_columns,
+          source_rows) == MagickFalse)
+        return(MagickFalse);
+      transform_image=FlipImage(*gainmap_image,exception);
+      return(ReplaceGainMapImage(gainmap_image,transform_image));
+    }
+  if (LocaleNCompare(transform,"flop ",5) == 0)
+    {
+      if (sscanf(transform+5,"%lfx%lf",&source_columns,&source_rows) != 2)
+        return(MagickFalse);
+      if (IsGainMapBaseGeometry(*base_columns,*base_rows,source_columns,
+          source_rows) == MagickFalse)
+        return(MagickFalse);
+      transform_image=FlopImage(*gainmap_image,exception);
+      return(ReplaceGainMapImage(gainmap_image,transform_image));
+    }
+  if (LocaleNCompare(transform,"rotate ",7) == 0)
+    {
+      size_t
+        next_columns,
+        next_rows;
+
+      if (sscanf(transform+7,"%lfx%lf %lf",&source_columns,&source_rows,
+          &x) != 3)
+        return(MagickFalse);
+      if (IsGainMapBaseGeometry(*base_columns,*base_rows,source_columns,
+          source_rows) == MagickFalse)
+        return(MagickFalse);
+      rotations=CastDoubleToSizeT(x) % 4;
+      if (rotations == 0)
+        return(MagickTrue);
+      transform_image=IntegralRotateImage(*gainmap_image,rotations,exception);
+      if (ReplaceGainMapImage(gainmap_image,transform_image) == MagickFalse)
+        return(MagickFalse);
+      if ((rotations == 1) || (rotations == 3))
+        {
+          next_columns=(*base_rows);
+          next_rows=(*base_columns);
+          *base_columns=next_columns;
+          *base_rows=next_rows;
+        }
+      return(MagickTrue);
+    }
+  if (LocaleNCompare(transform,"transpose ",10) == 0)
+    {
+      size_t
+        next_columns;
+
+      if (sscanf(transform+10,"%lfx%lf",&source_columns,&source_rows) != 2)
+        return(MagickFalse);
+      if (IsGainMapBaseGeometry(*base_columns,*base_rows,source_columns,
+          source_rows) == MagickFalse)
+        return(MagickFalse);
+      transform_image=TransposeImage(*gainmap_image,exception);
+      if (ReplaceGainMapImage(gainmap_image,transform_image) == MagickFalse)
+        return(MagickFalse);
+      next_columns=(*base_rows);
+      *base_rows=(*base_columns);
+      *base_columns=next_columns;
+      return(MagickTrue);
+    }
+  if (LocaleNCompare(transform,"transverse ",11) == 0)
+    {
+      size_t
+        next_columns;
+
+      if (sscanf(transform+11,"%lfx%lf",&source_columns,&source_rows) != 2)
+        return(MagickFalse);
+      if (IsGainMapBaseGeometry(*base_columns,*base_rows,source_columns,
+          source_rows) == MagickFalse)
+        return(MagickFalse);
+      transform_image=TransverseImage(*gainmap_image,exception);
+      if (ReplaceGainMapImage(gainmap_image,transform_image) == MagickFalse)
+        return(MagickFalse);
+      next_columns=(*base_rows);
+      *base_rows=(*base_columns);
+      *base_columns=next_columns;
+      return(MagickTrue);
+    }
+  return(MagickFalse);
+}
+
+static StringInfo *EncodeBaseImageProfile(const ImageInfo *image_info,
+  Image *image,ExceptionInfo *exception)
+{
+  Image
+    *base_image;
+
+  ImageInfo
+    *base_info;
+
+  size_t
+    length;
+
+  StringInfo
+    *profile,
+    *removed_profile;
+
+  void
+    *blob;
+
+  base_image=CloneImage(image,0,0,MagickTrue,exception);
+  if (base_image == (Image *) NULL)
+    return((StringInfo *) NULL);
+  removed_profile=RemoveImageProfile(base_image,"hdrgm");
+  if (removed_profile != (StringInfo *) NULL)
+    removed_profile=DestroyStringInfo(removed_profile);
+  base_info=CloneImageInfo(image_info);
+  (void) CopyMagickString(base_info->filename,"JPEG:uhdr-base.jpg",
+    MagickPathExtent);
+  (void) CopyMagickString(base_info->magick,"JPEG",MagickPathExtent);
+  if (image->quality > 0)
+    base_info->quality=image->quality;
+  (void) CopyMagickString(base_image->magick,"JPEG",MagickPathExtent);
+  blob=ImageToBlob(base_info,base_image,&length,exception);
+  base_image=DestroyImage(base_image);
+  base_info=DestroyImageInfo(base_info);
+  if (blob == (void *) NULL)
+    {
+      (void) ThrowMagickException(exception,GetMagickModule(),CoderError,
+        "UnableToEncodeBaseImage","`%s'",image->filename);
+      return((StringInfo *) NULL);
+    }
+  profile=BlobToProfileStringInfo("uhdr-base",blob,length,exception);
+  blob=RelinquishMagickMemory(blob);
+  return(profile);
+}
+
+static StringInfo *TransformGainMapProfile(const ImageInfo *image_info,
+  const Image *image,const StringInfo *gainmap_profile,
+  MagickBooleanType *transform_status,ExceptionInfo *exception)
+{
+  const char
+    *option,
+    *transforms;
+
+  Image
+    *gainmap_images;
+
+  ImageInfo
+    *gainmap_info;
+
+  MagickBooleanType
+    status,
+    transformed,
+    transform_required;
+
+  size_t
+    base_columns,
+    base_rows,
+    length;
+
+  StringInfo
+    *profile;
+
+  void
+    *blob;
+
+  assert(transform_status != (MagickBooleanType *) NULL);
+  *transform_status=MagickTrue;
+  base_columns=GetHDRGMPropertySize(image,"BaseWidth",exception);
+  base_rows=GetHDRGMPropertySize(image,"BaseHeight",exception);
+  transforms=GetImageProperty(image,"hdrgm:Transform",exception);
+  transform_required=((transforms != (const char *) NULL) &&
+    (*transforms != '\0')) ? MagickTrue : MagickFalse;
+  if ((base_columns == 0) || (base_rows == 0))
+    {
+      if (transform_required != MagickFalse)
+        {
+          *transform_status=MagickFalse;
+          (void) ThrowMagickException(exception,GetMagickModule(),CoderError,
+            "UnableToTransformGainMap","`%s'",image->filename);
+        }
+      return((StringInfo *) NULL);
+    }
+  if ((base_columns != image->columns) || (base_rows != image->rows))
+    {
+      if (transform_required == MagickFalse)
+        {
+          *transform_status=MagickFalse;
+          (void) ThrowMagickException(exception,GetMagickModule(),CoderError,
+            "UnableToTransformGainMap","`%s'",image->filename);
+          return((StringInfo *) NULL);
+        }
+    }
+  if (transform_required == MagickFalse)
+    return((StringInfo *) NULL);
+  gainmap_info=CloneImageInfo(image_info);
+  (void) CopyMagickString(gainmap_info->filename,"JPEG:hdrgm.jpg",
+    MagickPathExtent);
+  (void) CopyMagickString(gainmap_info->magick,"JPEG",MagickPathExtent);
+  gainmap_images=BlobToImage(gainmap_info,GetStringInfoDatum(gainmap_profile),
+    GetStringInfoLength(gainmap_profile),exception);
+  if (gainmap_images == (Image *) NULL)
+    {
+      gainmap_info=DestroyImageInfo(gainmap_info);
+      *transform_status=MagickFalse;
+      (void) ThrowMagickException(exception,GetMagickModule(),CoderError,
+        "UnableToDecodeGainMap","`%s'",image->filename);
+      return((StringInfo *) NULL);
+    }
+  status=MagickTrue;
+  transformed=MagickFalse;
+  if ((transforms != (const char *) NULL) && (*transforms != '\0'))
+    {
+      char
+        *next,
+        *transform,
+        *transform_list;
+
+      transform_list=AcquireString(transforms);
+      for (transform=transform_list; transform != (char *) NULL; )
+      {
+        next=strchr(transform,';');
+        if (next != (char *) NULL)
+          *next++='\0';
+        if (*transform != '\0')
+          {
+            status=ApplyGainMapTransform(&gainmap_images,&base_columns,
+              &base_rows,image,transform,exception);
+            if (status == MagickFalse)
+              break;
+            transformed=MagickTrue;
+          }
+        transform=next;
+      }
+      transform_list=DestroyString(transform_list);
+    }
+  if ((status != MagickFalse) &&
+      ((base_columns != image->columns) || (base_rows != image->rows)))
+    status=MagickFalse;
+  if (status == MagickFalse)
+    {
+      gainmap_info=DestroyImageInfo(gainmap_info);
+      gainmap_images=DestroyImageList(gainmap_images);
+      *transform_status=MagickFalse;
+      (void) ThrowMagickException(exception,GetMagickModule(),CoderError,
+        "UnableToTransformGainMap","`%s'",image->filename);
+      return((StringInfo *) NULL);
+    }
+  if (transformed == MagickFalse)
+    {
+      gainmap_images=DestroyImageList(gainmap_images);
+      gainmap_info=DestroyImageInfo(gainmap_info);
+      return((StringInfo *) NULL);
+    }
+  option=GetImageOption(image_info,"uhdr:gainmap-quality");
+  if (option != (const char *) NULL)
+    gainmap_info->quality=StringToUnsignedLong(option);
+  else if (image->quality > 0)
+    gainmap_info->quality=image->quality;
+  (void) CopyMagickString(gainmap_images->magick,"JPEG",MagickPathExtent);
+  blob=ImageToBlob(gainmap_info,gainmap_images,&length,exception);
+  gainmap_images=DestroyImageList(gainmap_images);
+  gainmap_info=DestroyImageInfo(gainmap_info);
+  if (blob == (void *) NULL)
+    {
+      *transform_status=MagickFalse;
+      (void) ThrowMagickException(exception,GetMagickModule(),CoderError,
+        "UnableToEncodeGainMap","`%s'",image->filename);
+      return((StringInfo *) NULL);
+    }
+  profile=BlobToProfileStringInfo("hdrgm",blob,length,exception);
+  blob=RelinquishMagickMemory(blob);
+  if (profile == (StringInfo *) NULL)
+    {
+      *transform_status=MagickFalse;
+      (void) ThrowMagickException(exception,GetMagickModule(),CoderError,
+        "UnableToEncodeGainMap","`%s'",image->filename);
+    }
+  return(profile);
+}
+
 static MagickBooleanType WriteUHDRImage(const ImageInfo *image_info,
   Image *images,ExceptionInfo *exception)
 {
@@ -654,7 +1152,23 @@ static MagickBooleanType WriteUHDRImage(const ImageInfo *image_info,
     sdrImgDescriptor = { 0 };

   uhdr_mem_block_t
-    sdr_profile, hdr_profile;
+    sdr_profile = { 0 },
+    hdr_profile = { 0 };
+
+  StringInfo
+    *base_image_profile = (StringInfo *) NULL,
+    *resized_gainmap_profile = (StringInfo *) NULL;
+
+  uhdr_compressed_image_t
+    base_image = { 0 },
+    gainmap_image = { 0 };
+
+  uhdr_gainmap_metadata_t
+    gainmap_info = { 0 };
+
+  MagickBooleanType
+    gainmap_transform_status = MagickTrue,
+    preserve_gainmap = MagickFalse;

   assert(image_info != (const ImageInfo *) NULL);
   assert(image_info->signature == MagickCoreSignature);
@@ -674,12 +1188,32 @@ static MagickBooleanType WriteUHDRImage(const ImageInfo *image_info,
         Preserve gainmap+metadata (for transcoding). If these exist, we do not
         regenerate the gainmap.  Instead, we pass them directly to the encoder.
       */
-      uhdr_compressed_image_t gainmap_image = { 0 };
-      uhdr_gainmap_metadata_t gainmap_info = { 0 };
-
       /*
         Compressed image descriptor.
       */
+      resized_gainmap_profile=TransformGainMapProfile(image_info,image,
+        gainmap_profile,&gainmap_transform_status,exception);
+      if (gainmap_transform_status == MagickFalse)
+        {
+          (void) CloseBlob(image);
+          return(MagickFalse);
+        }
+      if (resized_gainmap_profile != (StringInfo *) NULL)
+        gainmap_profile=(const StringInfo *) resized_gainmap_profile;
+      base_image_profile=EncodeBaseImageProfile(image_info,image,exception);
+      if (base_image_profile == (StringInfo *) NULL)
+        {
+          if (resized_gainmap_profile != (StringInfo *) NULL)
+            resized_gainmap_profile=DestroyStringInfo(resized_gainmap_profile);
+          (void) CloseBlob(image);
+          return(MagickFalse);
+        }
+      base_image.data=(void *) GetStringInfoDatum(base_image_profile);
+      base_image.data_sz=GetStringInfoLength(base_image_profile);
+      base_image.capacity=base_image.data_sz;
+      base_image.cg=getImageColorGamut(&image->chromaticity);
+      base_image.ct=UHDR_CT_SRGB;
+      base_image.range=UHDR_CR_FULL_RANGE;
       gainmap_image.data=(void *) GetStringInfoDatum(gainmap_profile);
       gainmap_image.data_sz=GetStringInfoLength(gainmap_profile);
       gainmap_image.capacity=gainmap_image.data_sz;
@@ -691,7 +1225,7 @@ static MagickBooleanType WriteUHDRImage(const ImageInfo *image_info,
       */
       GetHDRGMProperty3("GainMapMax",max_content_boost[0],max_content_boost[1],
         max_content_boost[2]);
-      GetHDRGMProperty3("GainMapMax",min_content_boost[0],min_content_boost[1],
+      GetHDRGMProperty3("GainMapMin",min_content_boost[0],min_content_boost[1],
         min_content_boost[2]);
       GetHDRGMProperty3("Gamma",gamma[0],gamma[1],gamma[2]);
       GetHDRGMProperty3("OffsetSDR",offset_sdr[0],offset_sdr[1],offset_sdr[2]);
@@ -699,17 +1233,7 @@ static MagickBooleanType WriteUHDRImage(const ImageInfo *image_info,
       GetHDRGMProperty("HDRCapacityMin",hdr_capacity_min);
       GetHDRGMProperty("HDRCapacityMax",hdr_capacity_max);
       GetHDRGMPropertyInt("UseBaseColorGrade",use_base_cg);
-      /*
-        Set gainmap.
-      */
-      uhdr_codec_private_t *encoder = uhdr_create_encoder();
-      if (encoder == NULL)
-        {
-          ThrowMagickException(exception,GetMagickModule(),CoderError,
-            "FailedToCreateEncoder","`%s'",image->filename);
-          return(MagickFalse);
-        }
-      uhdr_enc_set_gainmap_image(encoder,&gainmap_image,&gainmap_info);
+      preserve_gainmap=MagickTrue;
     }

   const char
@@ -1049,20 +1573,38 @@ next_image:
     }                                                                                         \
   }

-    /* Configure hdr and sdr intents */
-    if (status != MagickFalse && hdrImgDescriptor.planes[UHDR_PLANE_Y])
-    {
-      CHECK_IF_ERR(uhdr_enc_set_raw_image(handle, &hdrImgDescriptor, UHDR_HDR_IMG))
-      if (hdr_profile.data_sz != 0)
-        CHECK_IF_ERR(uhdr_enc_set_exif_data(handle, &hdr_profile))
-    }
+    if (handle == NULL)
+      {
+        (void) ThrowMagickException(exception,GetMagickModule(),CoderError,
+          "FailedToCreateEncoder","`%s'",image->filename);
+        status=MagickFalse;
+      }

-    if (status != MagickFalse && sdrImgDescriptor.planes[UHDR_PLANE_Y])
-    {
-      CHECK_IF_ERR(uhdr_enc_set_raw_image(handle, &sdrImgDescriptor, UHDR_SDR_IMG))
-      if (sdr_profile.data_sz != 0)
-        CHECK_IF_ERR(uhdr_enc_set_exif_data(handle, &sdr_profile))
-    }
+    if ((status != MagickFalse) && (preserve_gainmap != MagickFalse))
+      {
+        CHECK_IF_ERR(uhdr_enc_set_compressed_image(handle,&base_image,
+          UHDR_BASE_IMG))
+        if (status != MagickFalse)
+          CHECK_IF_ERR(uhdr_enc_set_gainmap_image(handle,&gainmap_image,
+            &gainmap_info))
+      }
+    else
+      {
+        /* Configure hdr and sdr intents */
+        if (status != MagickFalse && hdrImgDescriptor.planes[UHDR_PLANE_Y])
+        {
+          CHECK_IF_ERR(uhdr_enc_set_raw_image(handle, &hdrImgDescriptor, UHDR_HDR_IMG))
+          if (hdr_profile.data_sz != 0)
+            CHECK_IF_ERR(uhdr_enc_set_exif_data(handle, &hdr_profile))
+        }
+
+        if (status != MagickFalse && sdrImgDescriptor.planes[UHDR_PLANE_Y])
+        {
+          CHECK_IF_ERR(uhdr_enc_set_raw_image(handle, &sdrImgDescriptor, UHDR_SDR_IMG))
+          if (sdr_profile.data_sz != 0)
+            CHECK_IF_ERR(uhdr_enc_set_exif_data(handle, &sdr_profile))
+        }
+      }

     /* Configure encoding settings */
     if (status != MagickFalse && image->quality > 0 && image->quality <= 100)
@@ -1071,31 +1613,31 @@ next_image:
     const char
       *option;

-    if (status != MagickFalse)
+    if ((status != MagickFalse) && (preserve_gainmap == MagickFalse))
     {
       option = GetImageOption(image_info, "uhdr:gainmap-quality");
       if (option != (const char *)NULL)
         CHECK_IF_ERR(uhdr_enc_set_quality(handle, atoi(option), UHDR_GAIN_MAP_IMG))
     }

-    if (status != MagickFalse)
+    if ((status != MagickFalse) && (preserve_gainmap == MagickFalse))
       CHECK_IF_ERR(uhdr_enc_set_using_multi_channel_gainmap(handle, 1))

-    if (status != MagickFalse)
+    if ((status != MagickFalse) && (preserve_gainmap == MagickFalse))
       CHECK_IF_ERR(uhdr_enc_set_gainmap_scale_factor(handle, 1))

-    if (status != MagickFalse)
+    if ((status != MagickFalse) && (preserve_gainmap == MagickFalse))
       CHECK_IF_ERR(uhdr_enc_set_preset(handle, UHDR_USAGE_BEST_QUALITY))

     /* Configure gainmap metadata */
-    if (status != MagickFalse)
+    if ((status != MagickFalse) && (preserve_gainmap == MagickFalse))
     {
       option = GetImageOption(image_info, "uhdr:gainmap-gamma");
       if (option != (const char *)NULL)
         CHECK_IF_ERR(uhdr_enc_set_gainmap_gamma(handle, atof(option)))
     }

-    if (status != MagickFalse)
+    if ((status != MagickFalse) && (preserve_gainmap == MagickFalse))
     {
       option = GetImageOption(image_info, "uhdr:gainmap-min-content-boost");
       float minContentBoost = option != (const char *)NULL ? atof(option) : FLT_MIN;
@@ -1107,7 +1649,7 @@ next_image:
         CHECK_IF_ERR(uhdr_enc_set_min_max_content_boost(handle, minContentBoost, maxContentBoost))
     }

-    if (status != MagickFalse)
+    if ((status != MagickFalse) && (preserve_gainmap == MagickFalse))
     {
       option = GetImageOption(image_info, "uhdr:target-display-peak-brightness");
       float targetDispPeakBrightness = option != (const char *)NULL ? atof(option) : -1.0f;
@@ -1140,6 +1682,12 @@ next_image:
   if (sdrImgDescriptor.planes[UHDR_PLANE_Y])
     RelinquishMagickMemory(sdrImgDescriptor.planes[UHDR_PLANE_Y]);

+  if (resized_gainmap_profile != (StringInfo *) NULL)
+    resized_gainmap_profile=DestroyStringInfo(resized_gainmap_profile);
+
+  if (base_image_profile != (StringInfo *) NULL)
+    base_image_profile=DestroyStringInfo(base_image_profile);
+
   return status;
 }
 #endif