Commit 9505e6f88 for imagemagick.org

commit 9505e6f886cbdc3459dccf7c6dcc76c444954f49
Author: Madars <mad182@gmail.com>
Date:   Mon Apr 6 16:38:24 2026 +0300

    Fix JXL animated export transparent blending and offset frames (#8656)

    * Fix JXL animated export transparent blending and offset frames

    - Enforced alpha_trait normalization using TransparentAlphaChannel for missing alpha channels to prevent solid white backgrounds when converting sequences containing mixed transparencies.
    - Removed strict geometry checks from JXLSameFrameType inside JXL encoder and moved buffer allocation natively into the sequential frame loop to accurately allocate dynamically varying dimensions.
    - Populated JxlFrameHeader layer info parameters (have_crop, crop_x0, etc) to appropriately support animation sub-frames.
    - Correctly parsed image->previous->dispose property to map ImageMagick's dispose rules straight into JXL's blendmode behaviors (JXL_BLEND_REPLACE vs JXL_BLEND_BLEND) safely on both color and alpha channels.

    * Variable name frame makes more sense than next in this context

    * Fix white areas in animated JXL exports by using OpaqueAlphaChannel instead of TransparentAlphaChannel

    * Rename next variable to frame in JXLSameFrameType

diff --git a/coders/jxl.c b/coders/jxl.c
index a40583713..3126c7f98 100644
--- a/coders/jxl.c
+++ b/coders/jxl.c
@@ -43,6 +43,7 @@
 #include "MagickCore/blob.h"
 #include "MagickCore/blob-private.h"
 #include "MagickCore/cache.h"
+#include "MagickCore/channel.h"
 #include "MagickCore/colorspace-private.h"
 #include "MagickCore/exception.h"
 #include "MagickCore/exception-private.h"
@@ -932,17 +933,13 @@ static inline float JXLGetDistance(float quality)
 }

 static inline MagickBooleanType JXLSameFrameType(const Image *image,
-  const Image *next)
+  const Image *frame)
 {
-  if (image->columns != next->columns)
+  if (image->depth != frame->depth)
     return(MagickFalse);
-  if (image->rows != next->rows)
+  if (image->alpha_trait != frame->alpha_trait)
     return(MagickFalse);
-  if (image->depth != next->depth)
-    return(MagickFalse);
-  if (image->alpha_trait != next->alpha_trait)
-    return(MagickFalse);
-  if (image->colorspace != next->colorspace)
+  if (image->colorspace != frame->colorspace)
     return(MagickFalse);
   return(MagickTrue);
 }
@@ -983,7 +980,7 @@ static MagickBooleanType WriteJXLImage(const ImageInfo *image_info,Image *image,
     status;

   MemoryInfo
-    *pixel_info;
+    *pixel_info = (MemoryInfo *) NULL;

   MemoryManagerInfo
     memory_manager_info;
@@ -1015,6 +1012,39 @@ static MagickBooleanType WriteJXLImage(const ImageInfo *image_info,Image *image,
   if ((IssRGBCompatibleColorspace(image->colorspace) == MagickFalse) &&
       (IsCMYKColorspace(image->colorspace) == MagickFalse))
     (void) TransformImageColorspace(image,sRGBColorspace,exception);
+  if ((image_info->adjoin != MagickFalse) &&
+      (GetNextImageInList(image) != (Image *) NULL))
+    {
+      Image
+        *frame;
+
+      MagickBooleanType
+        has_alpha;
+
+      size_t
+        depth;
+
+      depth=image->depth;
+      has_alpha=MagickFalse;
+      for (frame=image; frame != (Image *) NULL; frame=GetNextImageInList(frame))
+      {
+        if ((frame->alpha_trait & BlendPixelTrait) != 0)
+          has_alpha=MagickTrue;
+        if (frame->depth > depth)
+          depth=frame->depth;
+      }
+      for (frame=image; frame != (Image *) NULL; frame=GetNextImageInList(frame))
+      {
+        frame->depth=depth;
+        if (has_alpha != MagickFalse)
+          {
+            if ((frame->alpha_trait & BlendPixelTrait) == 0)
+              (void) SetImageAlphaChannel(frame,OpaqueAlphaChannel,exception);
+          }
+        if (frame->colorspace != image->colorspace)
+          (void) TransformImageColorspace(frame,image->colorspace,exception);
+      }
+    }
   /*
     Initialize JXL delegate library.
   */
@@ -1165,31 +1195,65 @@ static MagickBooleanType WriteJXLImage(const ImageInfo *image_info,Image *image,
       ((pixel_format.data_type == JXL_TYPE_FLOAT) ? sizeof(float) :
        (pixel_format.data_type == JXL_TYPE_UINT16) ? sizeof(short) :
        sizeof(char));
-  if (HeapOverflowSanityCheck(image->columns,channels_size) != MagickFalse)
-    {
-      JxlThreadParallelRunnerDestroy(runner);
-      JxlEncoderDestroy(jxl_info);
-      ThrowWriterException(ResourceLimitError,"MemoryAllocationFailed");
-    }
-  bytes_per_row=image->columns*channels_size;
-  pixel_info=AcquireVirtualMemory(bytes_per_row,image->rows*sizeof(*pixels));
-  if (pixel_info == (MemoryInfo *) NULL)
-    {
-      JxlThreadParallelRunnerDestroy(runner);
-      JxlEncoderDestroy(jxl_info);
-      ThrowWriterException(CoderError,"MemoryAllocationFailed");
-    }
   do
   {
     Image
       *next;

+    if (HeapOverflowSanityCheck(image->columns,channels_size) != MagickFalse)
+      {
+        (void) ThrowMagickException(exception,GetMagickModule(),CoderError,
+          "MemoryAllocationFailed","`%s'",image->filename);
+        status=MagickFalse;
+        break;
+      }
+    bytes_per_row=image->columns*channels_size;
+    pixel_info=AcquireVirtualMemory(bytes_per_row,image->rows*sizeof(*pixels));
+    if (pixel_info == (MemoryInfo *) NULL)
+      {
+        (void) ThrowMagickException(exception,GetMagickModule(),CoderError,
+          "MemoryAllocationFailed","`%s'",image->filename);
+        status=MagickFalse;
+        break;
+      }
+
     if (basic_info.have_animation == JXL_TRUE)
       {
+        JxlBlendInfo
+          alpha_blend_info;
+
         frame_header.duration=(uint32_t) image->delay;
+        if ((image->previous == (Image *) NULL) ||
+            (image->previous->dispose == BackgroundDispose) ||
+            (image->previous->dispose == PreviousDispose))
+          {
+            frame_header.layer_info.blend_info.blendmode=JXL_BLEND_REPLACE;
+            frame_header.layer_info.blend_info.source=0;
+          }
+        else
+          {
+            frame_header.layer_info.blend_info.blendmode=JXL_BLEND_BLEND;
+            frame_header.layer_info.blend_info.source=1;
+          }
+        frame_header.layer_info.save_as_reference=1;
+        if ((image->page.width != 0) && (image->page.height != 0))
+          {
+            frame_header.layer_info.have_crop=JXL_TRUE;
+            frame_header.layer_info.crop_x0=(int32_t) image->page.x;
+            frame_header.layer_info.crop_y0=(int32_t) image->page.y;
+            frame_header.layer_info.xsize=(uint32_t) image->columns;
+            frame_header.layer_info.ysize=(uint32_t) image->rows;
+          }
         jxl_status=JxlEncoderSetFrameHeader(frame_settings,&frame_header);
         if (jxl_status != JXL_ENC_SUCCESS)
           break;
+        if (basic_info.num_extra_channels > 0)
+          {
+            JxlEncoderInitBlendInfo(&alpha_blend_info);
+            alpha_blend_info.blendmode=frame_header.layer_info.blend_info.blendmode;
+            alpha_blend_info.source=frame_header.layer_info.blend_info.source;
+            (void) JxlEncoderSetExtraChannelBlendInfo(frame_settings,0,&alpha_blend_info);
+          }
       }
     pixels=(unsigned char *) GetVirtualMemoryBlob(pixel_info);
     if (IsGrayColorspace(image->colorspace) != MagickFalse)
@@ -1225,9 +1289,11 @@ static MagickBooleanType WriteJXLImage(const ImageInfo *image_info,Image *image,
        status=MagickFalse;
        break;
       }
+    pixel_info=RelinquishVirtualMemory(pixel_info);
     image=SyncNextImageInList(image);
   } while (image_info->adjoin != MagickFalse);
-  pixel_info=RelinquishVirtualMemory(pixel_info);
+  if (pixel_info != (MemoryInfo *) NULL)
+    pixel_info=RelinquishVirtualMemory(pixel_info);
   if (jxl_status == JXL_ENC_SUCCESS)
     {
       unsigned char