GraphicsMagick: Support writing lossy/lossless 12 bits, and loss...

GraphicsMagick Commits <[email protected]> Sun, 11 Feb 2024 14:28:25 -0600
Newsgroups gmane.comp.video.graphicsmagick.cvs
Message-ID <mailman.10894.1707683316.16032.graphicsmagick-commit@lists.sourceforge.net>
changeset f232b4698ba9 in /hg/GraphicsMagick
details: http://hg.GraphicsMagick.org/hg/GraphicsMagick?cmd=changeset;node=f232b4698ba9
summary: Support writing lossy/lossless 12 bits, and lossless 16-bit JPEG using libjpeg-turbo-3.0

diffstat:

 ChangeLog                              |   14 +
 VisualMagick/installer/inc/version.isx |    4 +-
 coders/jpeg.c                          |  570 +++++++++++++++++---------------
 configure                              |    5 +-
 configure.ac                           |    5 +-
 doc/options.imdoc                      |   27 +
 magick/enum_strings.c                  |    4 +-
 magick/magick_config.h.in              |    3 +
 magick/version.h                       |    4 +-
 utilities/gm.1                         |   37 ++-
 www/ChangeLog.html                     |   15 +
 www/GraphicsMagick.html                |   20 +
 www/gm.html                            |   20 +
 13 files changed, 456 insertions(+), 272 deletions(-)

diffs (truncated from 969 to 500 lines):

diff -r 0bdf6df7faf2 -r f232b4698ba9 ChangeLog
--- a/ChangeLog	Sat Feb 10 14:54:39 2024 -0600
+++ b/ChangeLog	Sun Feb 11 14:28:22 2024 -0600
@@ -1,3 +1,17 @@
+2024-02-11  Bob Friesenhahn  <[email protected]>
+
+	* magick/enum_strings.c (StringToCompressionType): Recognize
+	"LosslessJPEG" as indicating LosslessJPEGCompression.
+
+	* coders/jpeg.c (WriteJPEGImage): Added support for writing lossy
+	or lossless 12 bits, and lossless 16-bits using libjpeg-turbo-3.0.
+	Added the -define options 'jpeg:data-precision',
+	'jpeg:lossless-precision', and 'jpeg:lossless-predictor', to
+	provide control over the parameters used.  Please note that most
+	applications do not support lossless JPEG compression (and only
+	support a sample size of 8) so take care not to use these features
+	if it not known that all involved software can support them!
+
 2024-02-10  Bob Friesenhahn  <[email protected]>
 
 	* coders/jpeg.c (WriteJPEGImage): Do some preparation to implement
diff -r 0bdf6df7faf2 -r f232b4698ba9 VisualMagick/installer/inc/version.isx
--- a/VisualMagick/installer/inc/version.isx	Sat Feb 10 14:54:39 2024 -0600
+++ b/VisualMagick/installer/inc/version.isx	Sun Feb 11 14:28:22 2024 -0600
@@ -10,5 +10,5 @@
 
 #define public MagickPackageName "GraphicsMagick"
 #define public MagickPackageVersion "1.4"
-#define public MagickPackageVersionAddendum ".020240210"
-#define public MagickPackageReleaseDate "snapshot-20240210"
+#define public MagickPackageVersionAddendum ".020240211"
+#define public MagickPackageReleaseDate "snapshot-20240211"
diff -r 0bdf6df7faf2 -r f232b4698ba9 coders/jpeg.c
--- a/coders/jpeg.c	Sat Feb 10 14:54:39 2024 -0600
+++ b/coders/jpeg.c	Sun Feb 11 14:28:22 2024 -0600
@@ -1129,15 +1129,10 @@
   return save_quality;
 }
 
-/*
-  Format JPEG color space to a string.
-*/
-static void
-FormatJPEGColorSpace(const J_COLOR_SPACE colorspace,
-                     char *colorspace_name)
+static const char *JPEGColorSpaceToString(const J_COLOR_SPACE colorspace)
 {
   const char
-    *s = NULL;
+    *s;
 
   switch (colorspace)
     {
@@ -1161,7 +1156,17 @@
       s = "YCCK";
       break;
     }
-  (void) strlcpy(colorspace_name,s,MaxTextExtent);
+  return s;
+}
+
+/*
+  Format JPEG color space to a string buffer of length MaxTextExtent.
+*/
+static void
+FormatJPEGColorSpace(const J_COLOR_SPACE colorspace,
+                     char *colorspace_name)
+{
+  (void) strlcpy(colorspace_name,JPEGColorSpaceToString(colorspace),MaxTextExtent);
 }
 
 /*
@@ -2661,9 +2666,6 @@
   magick_jpeg_pixels_t
     jpeg_pixels; /* Contents freed by FreeMagickClientData() */
 
-  JSAMPROW
-    scanline[1];
-
   char
     *sampling_factors,
     *preserve_settings;
@@ -2677,9 +2679,6 @@
   register const PixelPacket
     *p;
 
-  register JSAMPLE
-    *q;
-
   register long
     x;
 
@@ -2744,6 +2743,7 @@
   */
   if ((value=AccessDefinition(image_info,"jpeg","max-warnings")))
     client_data->max_warning_count=strtol(value,(char **) NULL, 10);
+  client_data->jpeg_pixels = &jpeg_pixels;
   jpeg_info.client_data=(void *) client_data;
   if (setjmp(client_data->error_recovery) != 0)
     {
@@ -2917,14 +2917,6 @@
 
   jpeg_set_defaults(&jpeg_info);
 
-#if defined(HAVE_JPEG12_WRITE_SCANLINES) && HAVE_JPEG12_WRITE_SCANLINES
-#endif /* if defined(HAVE_JPEG12_WRITE_SCANLINES) && HAVE_JPEG12_WRITE_SCANLINES */
-
-#if defined(HAVE_JPEG16_WRITE_SCANLINES) && HAVE_JPEG16_WRITE_SCANLINES
-#if defined(HAVE_JPEG_ENABLE_LOSSLESS) && HAVE_JPEG_ENABLE_LOSSLESS
-#endif /* if defined(HAVE_JPEG_ENABLE_LOSSLESS) && HAVE_JPEG_ENABLE_LOSSLESS */
-#endif /* if defined(HAVE_JPEG16_WRITE_SCANLINES) && HAVE_JPEG16_WRITE_SCANLINES */
-
   /*
     Determine bit depth (valid range in 8-16).
   */
@@ -2940,6 +2932,38 @@
       sample_size=8;
     jpeg_info.data_precision=sample_size;
   }
+
+  /*
+    Allow the user to set/override the data precision (8/12/16)
+  */
+  if ((value=AccessDefinition(image_info,"jpeg","data-precision")))
+    {
+      unsigned int data_precision_prop = 0;
+      if (sscanf(value,"%u",&data_precision_prop) == 1)
+        {
+          switch(data_precision_prop)
+            {
+            default:
+            case 8:
+              jpeg_info.data_precision=8;
+              break;
+#if defined(HAVE_JPEG12_WRITE_SCANLINES) && HAVE_JPEG12_WRITE_SCANLINES
+            case 12:
+              jpeg_info.data_precision=12;
+              break;
+#endif /* if defined(HAVE_JPEG12_WRITE_SCANLINES) && HAVE_JPEG12_WRITE_SCANLINES */
+#if defined(HAVE_JPEG16_WRITE_SCANLINES) && HAVE_JPEG16_WRITE_SCANLINES
+#if defined(HAVE_JPEG_ENABLE_LOSSLESS) && HAVE_JPEG_ENABLE_LOSSLESS
+#if defined(C_LOSSLESS_SUPPORTED)
+            case 16:
+              jpeg_info.data_precision=16;
+              break;
+#endif /* if defined(C_LOSSLESS_SUPPORTED) */
+#endif /* if defined(HAVE_JPEG_ENABLE_LOSSLESS) && HAVE_JPEG_ENABLE_LOSSLESS */
+#endif /* if defined(HAVE_JPEG16_WRITE_SCANLINES) && HAVE_JPEG16_WRITE_SCANLINES */
+            }
+        }
+    }
   if ((image->x_resolution == 0) || (image->y_resolution == 0))
     {
       image->x_resolution=72.0;
@@ -3006,7 +3030,7 @@
 
     huffman_memory = 0;
 
-#ifdef C_ARITH_CODING_SUPPORTED
+#if defined(C_ARITH_CODING_SUPPORTED) && C_ARITH_CODING_SUPPORTED
     /*
       Allow the user to turn on/off arithmetic coder.
     */
@@ -3018,7 +3042,7 @@
           jpeg_info.arith_code = True;
       }
     if (!jpeg_info.arith_code)     /* jpeg_info.optimize_coding must not be set to enable arithmetic. */
-#endif
+#endif /* if defined(C_ARITH_CODING_SUPPORTED) && C_ARITH_CODING_SUPPORTED */
       {
         if ((value=AccessDefinition(image_info,"jpeg","optimize-coding")))
           {
@@ -3066,38 +3090,61 @@
     (void) LogMagickEvent(CoderEvent,GetMagickModule(),
                           "Interlace:  nonprogressive");
 #endif
-  if ((image->compression == LosslessJPEGCompression) ||
-      (quality > 100))
+  if (image->logging)
+    (void) LogMagickEvent(CoderEvent,GetMagickModule(),
+                          "Compression: %s",
+                          CompressionTypeToString(image->compression));
+  if (image->compression == LosslessJPEGCompression)
     {
-#if !defined(LIBJPEG_TURBO_VERSION_NUMBER) && defined(C_LOSSLESS_SUPPORTED)
-      if (quality < 100)
-        ThrowException(&image->exception,CoderWarning,
-                       LosslessToLossyJPEGConversion,(char *) NULL);
-      else
+#if defined(C_LOSSLESS_SUPPORTED)
         {
           int
             point_transform,
             predictor;
 
-          predictor=quality/100;  /* range 1-7 */
-          point_transform=quality % 20;  /* range 0-15 */
-          jpeg_simple_lossless(&jpeg_info,predictor,point_transform);
+          predictor=1;  /* range 1-7 */
+          point_transform=0;  /* range 0 to precision-1 */
+
+          /*
+            Right-shift the input samples by the specified number of
+            bits as a form of color quantization.  Useful range of 0
+            to precision-1.  Use zero for true lossless compression!
+           */
+          if ((value=AccessDefinition(image_info,"jpeg","lossless-precision")))
+            {
+              int point_transform_v = 0;
+              if ((sscanf(value,"%u",&point_transform_v) == 1) && (point_transform_v >= 0))
+                point_transform = point_transform_v;
+            }
+
+          if ((value=AccessDefinition(image_info,"jpeg","lossless-predictor")))
+            {
+              int predictor_v = predictor;
+              if ((sscanf(value,"%u",&predictor_v) == 1) && (predictor_v >= 0))
+                predictor = predictor_v;
+            }
+
           if (image->logging)
             {
               (void) LogMagickEvent(CoderEvent,GetMagickModule(),
-                                    "Compression: lossless");
+                "Compression: lossless");
+              (void) LogMagickEvent(CoderEvent,GetMagickModule(),
+                "DPCM Predictor: %d",predictor);
               (void) LogMagickEvent(CoderEvent,GetMagickModule(),
-                                    "Predictor: %d",predictor);
-              (void) LogMagickEvent(CoderEvent,GetMagickModule(),
-                                    "Point Transform: %d",point_transform);
+                "DPCM Point Transform: %d",point_transform);
             }
+#if !defined(LIBJPEG_TURBO_VERSION_NUMBER)
+          jpeg_simple_lossless(&jpeg_info, predictor, point_transform);
+#elif defined(LIBJPEG_TURBO_VERSION_NUMBER)
+          jpeg_enable_lossless(&jpeg_info, predictor, point_transform);
+#endif
         }
 #else
-      {
-        jpeg_set_quality(&jpeg_info,100,True);
-        if (image->logging)
-          (void) LogMagickEvent(CoderEvent,GetMagickModule(),"Quality: 100");
-      }
+        {
+          jpeg_set_quality(&jpeg_info,100,True);
+          if (image->logging)
+            (void) LogMagickEvent(CoderEvent,GetMagickModule(),"Quality: 100");
+        }
 #endif
     }
   else
@@ -3105,8 +3152,9 @@
       jpeg_set_quality(&jpeg_info,(int) quality,True);
       if (image->logging)
         (void) LogMagickEvent(CoderEvent,GetMagickModule(),"Quality: %lu",
-                              quality);
+          quality);
     }
+
   if (sampling_factors != (char *) NULL)
     {
       double
@@ -3269,8 +3317,39 @@
   /*
     Convert MIFF to JPEG raster pixels.
   */
-  jpeg_pixels.t.j=MagickAllocateResourceLimitedArray(JSAMPLE *,
-                                                     (size_t)jpeg_info.input_components*image->columns,sizeof(JSAMPLE));
+  switch (jpeg_info.data_precision)
+    {
+#if defined(HAVE_JPEG16_WRITE_SCANLINES) && HAVE_JPEG16_WRITE_SCANLINES
+#if defined(HAVE_JPEG_ENABLE_LOSSLESS) && HAVE_JPEG_ENABLE_LOSSLESS
+#if defined(C_LOSSLESS_SUPPORTED)
+    case 16:
+      jpeg_pixels.t.j16 =
+        MagickAllocateResourceLimitedClearedArray(J16SAMPLE *,
+                                                  jpeg_info.input_components,
+                                                  MagickArraySize(image->columns,
+                                                                  sizeof(J16SAMPLE)));
+      break;
+#endif /* if defined(C_LOSSLESS_SUPPORTED) */
+#endif /* if defined(HAVE_JPEG_ENABLE_LOSSLESS) && HAVE_JPEG_ENABLE_LOSSLESS */
+#endif /* if defined(HAVE_JPEG16_WRITE_SCANLINES) && HAVE_JPEG16_WRITE_SCANLINES */
+#if defined(HAVE_JPEG12_WRITE_SCANLINES) && HAVE_JPEG12_WRITE_SCANLINES
+    case 12:
+      jpeg_pixels.t.j12 =
+        MagickAllocateResourceLimitedClearedArray(J12SAMPLE *,
+                                                  jpeg_info.input_components,
+                                                  MagickArraySize(image->columns,
+                                                                  sizeof(J12SAMPLE)));
+      break;
+#endif /* if defined(HAVE_JPEG12_WRITE_SCANLINES) && HAVE_JPEG12_WRITE_SCANLINES */
+    default:
+      {
+        jpeg_pixels.t.j=
+          MagickAllocateResourceLimitedArray(JSAMPLE *,
+                                             jpeg_info.input_components,
+                                             MagickArraySize(image->columns,
+                                                             sizeof(JSAMPLE)));
+      }
+    }
   if (jpeg_pixels.t.v == (JSAMPLE *) NULL)
     {
       if (huffman_memory)
@@ -3278,233 +3357,200 @@
       ThrowJPEGWriterException(ResourceLimitError,MemoryAllocationFailed,image);
     }
   client_data->jpeg_pixels = &jpeg_pixels;
-  scanline[0]=(JSAMPROW) jpeg_pixels.t.j;
-  if (jpeg_info.data_precision > 8 && jpeg_info.data_precision <= 16)
-    { /* Deep pixels */
-      unsigned int
-        scale_short;
-
-      scale_short=65535U/MaxValueGivenBits(jpeg_info.data_precision);
-
-      if (jpeg_info.in_color_space == JCS_GRAYSCALE)
-        { /* Start deep JCS_GRAYSCALE */
-          if (image->logging)
-            (void) LogMagickEvent(CoderEvent,GetMagickModule(),
-                                  "Writing %d bit JCS_GRAYSCALE samples",
-                                  jpeg_info.data_precision);
-          for (y=0; y < (long) image->rows; y++)
-            {
-              p=AcquireImagePixels(image,0,y,image->columns,1,
-                                   &image->exception);
-              if (p == (const PixelPacket *) NULL)
-                break;
-              q=jpeg_pixels.t.j;
-              if (image->is_grayscale)
-                {
-                  for (x=0; x < (long) image->columns; x++)
-                    {
-                      *q++=(JSAMPLE) (ScaleQuantumToShort(GetGraySample(p))/
-                                      scale_short);
-                      p++;
-                    }
-                }
-              else
-                {
-                  for (x=0; x < (long) image->columns; x++)
-                    {
-                      *q++=(JSAMPLE)
-                        (ScaleQuantumToShort(PixelIntensityToQuantum(p))/scale_short);
-                      p++;
-                    }
-                }
-              (void) jpeg_write_scanlines(&jpeg_info,scanline,1);
-#if !USE_LIBJPEG_PROGRESS
-              if (QuantumTick(y,image->rows))
-                if (!MagickMonitorFormatted(y,image->rows,&image->exception,
-                                            SaveImageText,image->filename,
-                                            image->columns,image->rows))
-                  break;
-#endif /* !USE_LIBJPEG_PROGRESS */
-            }
-        } /* End deep JCS_GRAYSCALE */
-      else
-        if ((jpeg_info.in_color_space == JCS_RGB) ||
-            (jpeg_info.in_color_space == JCS_YCbCr))
-          {  /* Start deep JCS_RGB || JCS_YCbCr */
-            if (image->logging)
-              (void) LogMagickEvent(CoderEvent,GetMagickModule(),
-                                    "Writing %d bit JCS_RGB or "
-                                    "JCS_YCbCr samples",
-                                    jpeg_info.data_precision);
-            for (y=0; y < (long) image->rows; y++)
-              {
-                p=AcquireImagePixels(image,0,y,image->columns,1,
-                                     &image->exception);
-                if (p == (const PixelPacket *) NULL)
-                  break;
-                q=jpeg_pixels.t.j;
+
+  if (image->logging)
+    (void) LogMagickEvent(CoderEvent,GetMagickModule(),
+                          "Writing %u bit %s samples...",
+                          jpeg_info.data_precision,
+                          JPEGColorSpaceToString(jpeg_info.in_color_space));
+
+  for (y=0; y < (long) image->rows; y++)
+    {
+      p=AcquireImagePixels(image,0,y,image->columns,1,&image->exception);
+      if (p == (const PixelPacket *) NULL)
+        break;
+
+#if defined(HAVE_JPEG16_WRITE_SCANLINES) && HAVE_JPEG16_WRITE_SCANLINES
+#if defined(HAVE_JPEG_ENABLE_LOSSLESS) && HAVE_JPEG_ENABLE_LOSSLESS
+#if defined(C_LOSSLESS_SUPPORTED)
+      if (jpeg_info.data_precision == 16)
+        {
+          {
+            J16SAMPROW
+              scanline[1];
+
+            if (jpeg_info.in_color_space == JCS_GRAYSCALE)
+              { /* Start deep JCS_GRAYSCALE */
+                if (image->is_grayscale)
+                  {
+                    i=0;
+                    for (x=0; x < (long) image->columns; x++)
+                      {
+                        jpeg_pixels.t.j16[i++]=(J16SAMPLE)(ScaleQuantumToShort(GetGraySample(p)));
+                        p++;
+                      }
+                  }
+                else
+                  {
+                    i=0;
+                    for (x=0; x < (long) image->columns; x++)
+                      {
+                        jpeg_pixels.t.j16[i++] = (J16SAMPLE)(ScaleQuantumToShort(PixelIntensityToQuantum(p)));
+                        p++;
+                      }
+                  }
+              } /* End deep JCS_GRAYSCALE */
+            else if ((jpeg_info.in_color_space == JCS_RGB) ||
+                     (jpeg_info.in_color_space == JCS_YCbCr))
+              { /* Start deep JCS_RGB || JCS_YCbCr */
+                i=0;
                 for (x=0; x < (long) image->columns; x++)
                   {
-                    *q++=(JSAMPLE) (ScaleQuantumToShort(p->red)/16);
-                    *q++=(JSAMPLE) (ScaleQuantumToShort(p->green)/16);
-                    *q++=(JSAMPLE) (ScaleQuantumToShort(p->blue)/16);
+                    jpeg_pixels.t.j16[i++] = (J16SAMPLE)(ScaleQuantumToShort(p->red));
+                    jpeg_pixels.t.j16[i++] = (J16SAMPLE)(ScaleQuantumToShort(p->green));
+                    jpeg_pixels.t.j16[i++] = (J16SAMPLE)(ScaleQuantumToShort(p->blue));
                     p++;
                   }
-                (void) jpeg_write_scanlines(&jpeg_info,scanline,1);
-#if !USE_LIBJPEG_PROGRESS
-                if (QuantumTick(y,image->rows))
-                  if (!MagickMonitorFormatted(y,image->rows,&image->exception,
-                                              SaveImageText,image->filename,
-                                              image->columns,image->rows))
-                    break;
-#endif /* !USE_LIBJPEG_PROGRESS */
-              }
-          } /* End deep JCS_RGB || JCS_YCbCr */
-        else
-          { /* Start deep JCS_CMYK */
-            if (image->logging)
-              (void) LogMagickEvent(CoderEvent,GetMagickModule(),
-                                    "Writing %d bit JCS_CMYK samples",
-                                    jpeg_info.data_precision);
-            for (y=0; y < (long) image->rows; y++)
-              {
-                p=AcquireImagePixels(image,0,y,image->columns,1,
-                                     &image->exception);
-                if (p == (const PixelPacket *) NULL)
-                  break;
-                q=jpeg_pixels.t.j;
+              } /* End deep JCS_RGB || JCS_YCbCr */
+            else if (jpeg_info.in_color_space == JCS_CMYK)
+              { /* Start deep JCS_CMYK */
+                i=0;
                 for (x=0; x < (long) image->columns; x++)
                   {
-                    /*
-                      Convert DirectClass packets to contiguous CMYK scanlines.
-                    */
-                    *q++=(JSAMPLE) (4095-ScaleQuantumToShort(p->red)/16);
-                    *q++=(JSAMPLE) (4095-ScaleQuantumToShort(p->green)/16);
-                    *q++=(JSAMPLE) (4095-ScaleQuantumToShort(p->blue)/16);
-                    *q++=(JSAMPLE) (4095-ScaleQuantumToShort(p->opacity)/16);
+                    jpeg_pixels.t.j16[i++] = (J16SAMPLE)(ScaleQuantumToShort(p->red));
+                    jpeg_pixels.t.j16[i++] = (J16SAMPLE)(ScaleQuantumToShort(p->green));
+                    jpeg_pixels.t.j16[i++] = (J16SAMPLE)(ScaleQuantumToShort(p->blue));
+                    jpeg_pixels.t.j16[i++] = (J16SAMPLE)(ScaleQuantumToShort(p->opacity));
                     p++;
                   }
-                (void) jpeg_write_scanlines(&jpeg_info,scanline,1);
+              } /* End deep JCS_CMYK */
+
+            scanline[0]=(J16SAMPROW) jpeg_pixels.t.j16;
+            (void) jpeg16_write_scanlines(&jpeg_info,scanline,1);
+          }
+        } else
+#endif /* if defined(C_LOSSLESS_SUPPORTED) */
+#endif /* if defined(HAVE_JPEG_ENABLE_LOSSLESS) && HAVE_JPEG_ENABLE_LOSSLESS */
+#endif /* if defined(HAVE_JPEG16_WRITE_SCANLINES) && HAVE_JPEG16_WRITE_SCANLINES */
+
+#if defined(HAVE_JPEG12_WRITE_SCANLINES) && HAVE_JPEG12_WRITE_SCANLINES
+        if (jpeg_info.data_precision == 12)
+          {
+            J12SAMPROW
+              scanline[1];
+
+            if (jpeg_info.in_color_space == JCS_GRAYSCALE)
+              { /* Start deep JCS_GRAYSCALE */
+                if (image->is_grayscale)
+                  {
+                    i=0;
+                    for (x=0; x < (long) image->columns; x++)
+                      {
+                        jpeg_pixels.t.j12[i++] = (J12SAMPLE)(ScaleQuantumToShort(GetGraySample(p))/16);
+                        p++;