diff mupdf-source/thirdparty/lcms2/utils/jpgicc/jpgicc.c @ 2:b50eed0cc0ef upstream

ADD: MuPDF v1.26.7: the MuPDF source as downloaded by a default build of PyMuPDF 1.26.4. The directory name has changed: no version number in the expanded directory now.
author Franz Glasner <fzglas.hg@dom66.de>
date Mon, 15 Sep 2025 11:43:07 +0200
parents
children
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mupdf-source/thirdparty/lcms2/utils/jpgicc/jpgicc.c	Mon Sep 15 11:43:07 2025 +0200
@@ -0,0 +1,1281 @@
+//---------------------------------------------------------------------------------
+//
+//  Little Color Management System
+//  Copyright (c) 1998-2023 Marti Maria Saguer
+//
+// Permission is hereby granted, free of charge, to any person obtaining
+// a copy of this software and associated documentation files (the "Software"),
+// to deal in the Software without restriction, including without limitation
+// the rights to use, copy, modify, merge, publish, distribute, sublicense,
+// and/or sell copies of the Software, and to permit persons to whom the Software
+// is furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO
+// THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+//
+
+// This program does apply profiles to (some) JPEG files
+
+
+#include "utils.h"
+
+#include "jpeglib.h"
+#include "iccjpeg.h"
+
+// Flags
+static cmsBool BlackPointCompensation = FALSE;
+static cmsBool IgnoreEmbedded         = FALSE;
+static cmsBool GamutCheck             = FALSE;
+static cmsBool lIsITUFax              = FALSE;
+static cmsBool lIsPhotoshopApp13      = FALSE;
+static cmsBool lIsEXIF;
+static cmsBool lIsDeviceLink          = FALSE;
+static cmsBool EmbedProfile           = FALSE;
+
+static const char* SaveEmbedded = NULL;
+
+static int Intent                  = INTENT_PERCEPTUAL;
+static int ProofingIntent          = INTENT_PERCEPTUAL;
+static int PrecalcMode             = 1;
+
+static int jpegQuality             = 75;
+
+static cmsFloat64Number ObserverAdaptationState = 0;
+
+
+static char *cInpProf  = NULL;
+static char *cOutProf  = NULL;
+static char *cProofing = NULL;
+
+static FILE * InFile;
+static FILE * OutFile;
+
+static struct jpeg_decompress_struct Decompressor;
+static struct jpeg_compress_struct   Compressor;
+
+
+static struct my_error_mgr {
+
+    struct  jpeg_error_mgr pub;   // "public" fields
+    void*   Cargo;                // "private" fields
+
+} ErrorHandler;
+
+
+cmsUInt16Number Alarm[cmsMAXCHANNELS] = {128,128,128,0};
+
+
+static
+void my_error_exit (j_common_ptr cinfo)
+{
+  char buffer[JMSG_LENGTH_MAX];
+
+  (*cinfo->err->format_message) (cinfo, buffer);
+  FatalError(buffer);
+}
+
+/*
+Definition of the APPn Markers Defined for continuous-tone G3FAX
+
+The application code APP1 initiates identification of the image as
+a G3FAX application and defines the spatial resolution and subsampling.
+This marker directly follows the SOI marker. The data format will be as follows:
+
+X'FFE1' (APP1), length, FAX identifier, version, spatial resolution.
+
+The above terms are defined as follows:
+
+Length: (Two octets) Total APP1 field octet count including the octet count itself, but excluding the APP1
+marker.
+
+FAX identifier: (Six octets) X'47', X'33', X'46', X'41', X'58', X'00'. This X'00'-terminated string "G3FAX"
+uniquely identifies this APP1 marker.
+
+Version: (Two octets) X'07CA'. This string specifies the year of approval of the standard, for identification
+in the case of future revision (for example, 1994).
+
+Spatial Resolution: (Two octets) Lightness pixel density in pels/25.4 mm. The basic value is 200. Allowed values are
+100, 200, 300, 400, 600 and 1200 pels/25.4 mm, with square (or equivalent) pels.
+
+NOTE - The functional equivalence of inch-based and mm-based resolutions is maintained. For example, the 200 x 200
+*/
+
+static
+cmsBool IsITUFax(jpeg_saved_marker_ptr ptr)
+{
+    while (ptr)
+    {
+        if (ptr -> marker == (JPEG_APP0 + 1) && ptr -> data_length > 5) {
+
+            const char* data = (const char*) ptr -> data;
+
+            if (strcmp(data, "G3FAX") == 0) return TRUE;
+        }
+
+        ptr = ptr -> next;
+    }
+
+    return FALSE;
+}
+
+// Save a ITU T.42/Fax marker with defaults on boundaries. This is the only mode we support right now.
+static
+void SetITUFax(j_compress_ptr cinfo)
+{
+    unsigned char Marker[] = "G3FAX\x00\0x07\xCA\x00\xC8";
+
+    jpeg_write_marker(cinfo, (JPEG_APP0 + 1), Marker, 10);
+}
+
+
+// Build a profile for decoding ITU T.42/Fax JPEG streams.
+// The profile has an additional ability in the input direction of
+// gamut compress values between 85 < a < -85 and -75 < b < 125. This conforms
+// the default range for ITU/T.42 -- See RFC 2301, section 6.2.3 for details
+
+//  L*  =   [0, 100]
+//  a*  =   [-85, 85]
+//  b*  =   [-75, 125]
+
+
+// These functions does convert the encoding of ITUFAX to floating point
+// and vice-versa. No gamut mapping is performed yet.
+
+static
+void ITU2Lab(const cmsUInt16Number In[3], cmsCIELab* Lab)
+{
+    Lab -> L = (double) In[0] / 655.35;
+    Lab -> a = (double) 170.* (In[1] - 32768.) / 65535.;
+    Lab -> b = (double) 200.* (In[2] - 24576.) / 65535.;
+}
+
+static
+void Lab2ITU(const cmsCIELab* Lab, cmsUInt16Number Out[3])
+{
+    Out[0] = (cmsUInt16Number) floor((double) (Lab -> L / 100.)* 65535. );
+    Out[1] = (cmsUInt16Number) floor((double) (Lab -> a / 170.)* 65535. + 32768. );
+    Out[2] = (cmsUInt16Number) floor((double) (Lab -> b / 200.)* 65535. + 24576. );
+}
+
+// These are the samplers-- They are passed as callbacks to cmsStageSampleCLut16bit()
+// then, cmsSample3DGrid() will sweel whole Lab gamut calling these functions
+// once for each node. In[] will contain the Lab PCS value to convert to ITUFAX
+// on PCS2ITU, or the ITUFAX value to convert to Lab in ITU2PCS
+// You can change the number of sample points if desired, the algorithm will
+// remain same. 33 points gives good accuracy, but you can reduce to 22 or less
+// is space is critical
+
+#define GRID_POINTS 33
+
+static
+int PCS2ITU(cmsContext ContextID, register const cmsUInt16Number In[], register cmsUInt16Number Out[], register void*  Cargo)
+{
+    cmsCIELab Lab;
+
+    cmsLabEncoded2Float(NULL, &Lab, In);
+    cmsDesaturateLab(NULL, &Lab, 85, -85, 125, -75);    // This function does the necessary gamut remapping
+    Lab2ITU(&Lab, Out);
+    return TRUE;
+
+    UTILS_UNUSED_PARAMETER(Cargo);
+    UTILS_UNUSED_PARAMETER(ContextID);
+}
+
+
+static
+int ITU2PCS(cmsContext ContextID, register const cmsUInt16Number In[], register cmsUInt16Number Out[], register void*  Cargo)
+{
+    cmsCIELab Lab;
+
+    ITU2Lab(In, &Lab);
+    cmsFloat2LabEncoded(NULL, Out, &Lab);
+    return TRUE;
+
+    UTILS_UNUSED_PARAMETER(Cargo);
+    UTILS_UNUSED_PARAMETER(ContextID);
+}
+
+// This function does create the virtual input profile, which decodes ITU to the profile connection space
+static
+cmsHPROFILE CreateITU2PCS_ICC(void)
+{
+    cmsHPROFILE hProfile;
+    cmsPipeline* AToB0;
+    cmsStage* ColorMap;
+
+    AToB0 = cmsPipelineAlloc(0, 3, 3);
+    if (AToB0 == NULL) return NULL;
+
+    ColorMap = cmsStageAllocCLut16bit(0, GRID_POINTS, 3, 3, NULL);
+    if (ColorMap == NULL) return NULL;
+
+    cmsPipelineInsertStage(NULL, AToB0, cmsAT_BEGIN, ColorMap);
+    cmsStageSampleCLut16bit(NULL, ColorMap, ITU2PCS, NULL, 0);
+
+    hProfile = cmsCreateProfilePlaceholder(0);
+    if (hProfile == NULL) {
+        cmsPipelineFree(NULL, AToB0);
+        return NULL;
+    }
+
+    cmsWriteTag(NULL, hProfile, cmsSigAToB0Tag, AToB0);
+    cmsSetColorSpace(NULL, hProfile, cmsSigLabData);
+    cmsSetPCS(NULL, hProfile, cmsSigLabData);
+    cmsSetDeviceClass(NULL, hProfile, cmsSigColorSpaceClass);
+    cmsPipelineFree(NULL, AToB0);
+
+    return hProfile;
+}
+
+
+// This function does create the virtual output profile, with the necessary gamut mapping
+static
+cmsHPROFILE CreatePCS2ITU_ICC(void)
+{
+    cmsHPROFILE hProfile;
+    cmsPipeline* BToA0;
+    cmsStage* ColorMap;
+
+    BToA0 = cmsPipelineAlloc(0, 3, 3);
+    if (BToA0 == NULL) return NULL;
+
+    ColorMap = cmsStageAllocCLut16bit(0, GRID_POINTS, 3, 3, NULL);
+    if (ColorMap == NULL) return NULL;
+
+    cmsPipelineInsertStage(NULL, BToA0, cmsAT_BEGIN, ColorMap);
+    cmsStageSampleCLut16bit(NULL, ColorMap, PCS2ITU, NULL, 0);
+
+    hProfile = cmsCreateProfilePlaceholder(0);
+    if (hProfile == NULL) {
+        cmsPipelineFree(NULL, BToA0);
+        return NULL;
+    }
+
+    cmsWriteTag(NULL, hProfile, cmsSigBToA0Tag, BToA0);
+    cmsSetColorSpace(NULL, hProfile, cmsSigLabData);
+    cmsSetPCS(NULL, hProfile, cmsSigLabData);
+    cmsSetDeviceClass(NULL, hProfile, cmsSigColorSpaceClass);
+
+    cmsPipelineFree(NULL, BToA0);
+
+    return hProfile;
+}
+
+
+
+#define PS_FIXED_TO_FLOAT(h, l) ((float) (h) + ((float) (l)/(1<<16)))
+
+static
+cmsBool ProcessPhotoshopAPP13(JOCTET *data, int datalen)
+{
+    int i;
+
+    for (i = 14; i < datalen; )
+    {
+        long len;
+        unsigned int type;
+
+        if (!(GETJOCTET(data[i]  ) == 0x38 &&
+              GETJOCTET(data[i+1]) == 0x42 &&
+              GETJOCTET(data[i+2]) == 0x49 &&
+              GETJOCTET(data[i+3]) == 0x4D)) break; // Not recognized
+
+        i += 4; // identifying string
+
+        type = (unsigned int) (GETJOCTET(data[i]<<8) + GETJOCTET(data[i+1]));
+
+        i += 2; // resource type
+
+        i += GETJOCTET(data[i]) + ((GETJOCTET(data[i]) & 1) ? 1 : 2);   // resource name
+
+        len = ((((GETJOCTET(data[i]<<8) + GETJOCTET(data[i+1]))<<8) +
+                         GETJOCTET(data[i+2]))<<8) + GETJOCTET(data[i+3]);
+
+        if (len < 0) return FALSE; // Keep bug hunters away
+
+        i += 4; // Size
+
+        if (type == 0x03ED && len >= 16) {
+
+            Decompressor.X_density = (UINT16) PS_FIXED_TO_FLOAT(GETJOCTET(data[i]<<8) + GETJOCTET(data[i+1]),
+                                                 GETJOCTET(data[i+2]<<8) + GETJOCTET(data[i+3]));
+            Decompressor.Y_density = (UINT16) PS_FIXED_TO_FLOAT(GETJOCTET(data[i+8]<<8) + GETJOCTET(data[i+9]),
+                                                 GETJOCTET(data[i+10]<<8) + GETJOCTET(data[i+11]));
+
+            // Set the density unit to 1 since the
+            // Vertical and Horizontal resolutions
+            // are specified in Pixels per inch
+
+            Decompressor.density_unit = 0x01;
+            return TRUE;
+
+        }
+
+        i += len + ((len & 1) ? 1 : 0);   // Alignment
+    }
+    return FALSE;
+}
+
+
+static
+cmsBool HandlePhotoshopAPP13(jpeg_saved_marker_ptr ptr)
+{
+    while (ptr) {
+
+        if (ptr -> marker == (JPEG_APP0 + 13) && ptr -> data_length > 9)
+        {
+            JOCTET* data = ptr -> data;
+
+            if(GETJOCTET(data[0]) == 0x50 &&
+               GETJOCTET(data[1]) == 0x68 &&
+               GETJOCTET(data[2]) == 0x6F &&
+               GETJOCTET(data[3]) == 0x74 &&
+               GETJOCTET(data[4]) == 0x6F &&
+               GETJOCTET(data[5]) == 0x73 &&
+               GETJOCTET(data[6]) == 0x68 &&
+               GETJOCTET(data[7]) == 0x6F &&
+               GETJOCTET(data[8]) == 0x70) {
+
+                ProcessPhotoshopAPP13(data, ptr -> data_length);
+                return TRUE;
+            }
+        }
+
+        ptr = ptr -> next;
+    }
+
+    return FALSE;
+}
+
+
+typedef unsigned short uint16_t;
+typedef unsigned char uint8_t;
+typedef unsigned int uint32_t;
+
+#define INTEL_BYTE_ORDER 0x4949
+#define XRESOLUTION 0x011a
+#define YRESOLUTION 0x011b
+#define RESOLUTION_UNIT 0x128
+
+// Abort if crafted file
+static
+void craftedFile(void)
+{
+    FatalError("Corrupted EXIF data");
+}
+
+// Read a 16-bit word
+static
+uint16_t read16(uint8_t* arr, size_t pos,  int swapBytes, size_t max)
+{
+    if (pos + 2 >= max)
+    {
+        craftedFile();
+        return 0;
+    }
+    else
+    {
+        uint8_t b1 = arr[pos];
+        uint8_t b2 = arr[pos + 1];
+
+        return (swapBytes) ? ((b2 << 8) | b1) : ((b1 << 8) | b2);
+    }
+}
+
+
+// Read a 32-bit word
+static
+uint32_t read32(uint8_t* arr, size_t pos, int swapBytes, size_t max)
+{
+
+    if (pos + 4 >= max)
+    {
+        craftedFile();
+        return 0;
+    }
+    else
+    {
+        if (!swapBytes) {
+
+            return (arr[pos] << 24) | (arr[pos + 1] << 16) | (arr[pos + 2] << 8) | arr[pos + 3];
+        }
+
+        return arr[pos] | (arr[pos + 1] << 8) | (arr[pos + 2] << 16) | (arr[pos + 3] << 24);
+    }
+}
+
+
+
+static
+int read_tag(uint8_t* arr, int pos,  int swapBytes, void* dest, size_t max)
+{
+        // Format should be 5 over here (rational)
+    uint32_t format = read16(arr, pos + 2, swapBytes, max);
+    // Components should be 1
+    uint32_t components = read32(arr, pos + 4, swapBytes, max);
+    // Points to the value
+    uint32_t offset;
+
+    // sanity
+    if (components != 1) return 0;
+
+    if (format == 3)
+        offset = pos + 8;
+    else
+        offset =  read32(arr, pos + 8, swapBytes, max);
+
+    switch (format) {
+
+    case 5: // Rational
+          {
+          double num = read32(arr, offset, swapBytes, max);
+          double den = read32(arr, offset + 4, swapBytes, max);
+          *(double *) dest = num / den;
+          }
+          break;
+
+    case 3: // uint 16
+        *(int*) dest = read16(arr, offset, swapBytes, max);
+        break;
+
+    default:  return 0;
+    }
+
+    return 1;
+}
+
+
+
+// Handler for EXIF data
+static
+cmsBool HandleEXIF(struct jpeg_decompress_struct* cinfo)
+{
+    jpeg_saved_marker_ptr ptr;
+    uint32_t ifd_ofs;
+    int pos = 0, swapBytes = 0;
+    uint32_t i, numEntries;
+    double XRes = -1, YRes = -1;
+    int Unit = 2; // Inches
+
+
+    for (ptr = cinfo ->marker_list; ptr; ptr = ptr ->next) {
+
+        if ((ptr ->marker == JPEG_APP0+1) && ptr ->data_length > 6) {
+
+            JOCTET* data = ptr -> data;
+            size_t max = ptr->data_length;
+
+            if (memcmp(data, "Exif\0\0", 6) == 0) {
+
+                data += 6; // Skip EXIF marker
+
+                // 8 byte TIFF header
+                // first two determine byte order
+                pos = 0;
+                if (read16(data, pos, 0, max) == INTEL_BYTE_ORDER) {
+                    swapBytes = 1;
+                }
+
+                pos += 2;
+
+                // next two bytes are always 0x002A (TIFF version)
+                pos += 2;
+
+                // offset to Image File Directory (includes the previous 8 bytes)
+                ifd_ofs = read32(data, pos, swapBytes, max);
+
+                // Search the directory for resolution tags
+                numEntries = read16(data, ifd_ofs, swapBytes, max);
+
+                for (i=0; i < numEntries; i++) {
+
+                    uint32_t entryOffset = ifd_ofs + 2 + (12 * i);
+                    uint32_t tag = read16(data, entryOffset, swapBytes, max);
+
+                    switch (tag) {
+
+                    case RESOLUTION_UNIT:
+                        if (!read_tag(data, entryOffset, swapBytes, &Unit, max)) return FALSE;
+                        break;
+
+                    case XRESOLUTION:
+                        if (!read_tag(data, entryOffset, swapBytes, &XRes, max)) return FALSE;
+                        break;
+
+                    case YRESOLUTION:
+                        if (!read_tag(data, entryOffset, swapBytes, &YRes, max)) return FALSE;
+                        break;
+
+                    default:;
+                    }
+
+                }
+
+                // Proceed if all found
+
+                if (XRes != -1 && YRes != -1)
+                {
+
+                    // 1 = None
+                    // 2 = inches
+                    // 3 = cm
+
+                    switch (Unit) {
+
+                    case 2:
+
+                        cinfo ->X_density = (UINT16) floor(XRes + 0.5);
+                        cinfo ->Y_density = (UINT16) floor(YRes + 0.5);
+                        break;
+
+                    case 1:
+
+                        cinfo ->X_density = (UINT16) floor(XRes * 2.54 + 0.5);
+                        cinfo ->Y_density = (UINT16) floor(YRes * 2.54 + 0.5);
+                        break;
+
+                    default: return FALSE;
+                    }
+
+                    cinfo ->density_unit = 1;  /* 1 for dots/inch, or 2 for dots/cm.*/
+
+                }
+
+
+            }
+        }
+    }
+    return FALSE;
+}
+
+
+static
+cmsBool OpenInput(const char* FileName)
+{
+    int m;
+
+    lIsITUFax = FALSE;
+    InFile  = fopen(FileName, "rb");
+    if (InFile == NULL) {
+        FatalError("Cannot open '%s'", FileName);
+    }
+
+    // Now we can initialize the JPEG decompression object.
+    Decompressor.err                 = jpeg_std_error(&ErrorHandler.pub);
+    ErrorHandler.pub.error_exit      = my_error_exit;
+    ErrorHandler.pub.output_message  = my_error_exit;
+
+    jpeg_create_decompress(&Decompressor);
+    jpeg_stdio_src(&Decompressor, InFile);
+
+    for (m = 0; m < 16; m++)
+        jpeg_save_markers(&Decompressor, JPEG_APP0 + m, 0xFFFF);
+
+    // setup_read_icc_profile(&Decompressor);
+
+    fseek(InFile, 0, SEEK_SET);
+    jpeg_read_header(&Decompressor, TRUE);
+
+    return TRUE;
+}
+
+
+static
+cmsBool OpenOutput(const char* FileName)
+{
+
+    OutFile = fopen(FileName, "wb");
+    if (OutFile == NULL) {
+        FatalError("Cannot create '%s'", FileName);
+
+    }
+
+    Compressor.err                   = jpeg_std_error(&ErrorHandler.pub);
+    ErrorHandler.pub.error_exit      = my_error_exit;
+    ErrorHandler.pub.output_message  = my_error_exit;
+
+    Compressor.input_components = Compressor.num_components = 4;
+
+    jpeg_create_compress(&Compressor);
+    jpeg_stdio_dest(&Compressor, OutFile);
+    return TRUE;
+}
+
+static
+cmsBool Done(void)
+{
+    jpeg_destroy_decompress(&Decompressor);
+    jpeg_destroy_compress(&Compressor);
+    return fclose(InFile) + fclose(OutFile);
+
+}
+
+
+// Build up the pixeltype descriptor
+
+static
+cmsUInt32Number GetInputPixelType(void)
+{
+     int space, bps, extra, ColorChannels, Flavor;
+
+     lIsITUFax         = IsITUFax(Decompressor.marker_list);
+     lIsPhotoshopApp13 = HandlePhotoshopAPP13(Decompressor.marker_list);
+     lIsEXIF           = HandleEXIF(&Decompressor);
+
+     ColorChannels = Decompressor.num_components;
+     extra  = 0;            // Alpha = None
+     bps    = 1;            // 8 bits
+     Flavor = 0;            // Vanilla
+
+     if (lIsITUFax) {
+
+        space = PT_Lab;
+        Decompressor.out_color_space = JCS_YCbCr;  // Fake to don't touch
+     }
+     else
+     switch (Decompressor.jpeg_color_space) {
+
+     case JCS_GRAYSCALE:        // monochrome
+              space = PT_GRAY;
+              Decompressor.out_color_space = JCS_GRAYSCALE;
+              break;
+
+     case JCS_RGB:             // red/green/blue
+              space = PT_RGB;
+              Decompressor.out_color_space = JCS_RGB;
+              break;
+
+     case JCS_YCbCr:               // Y/Cb/Cr (also known as YUV)
+              space = PT_RGB;      // Let IJG code to do the conversion
+              Decompressor.out_color_space = JCS_RGB;
+              break;
+
+     case JCS_CMYK:            // C/M/Y/K
+              space = PT_CMYK;
+              Decompressor.out_color_space = JCS_CMYK;
+              if (Decompressor.saw_Adobe_marker)            // Adobe keeps CMYK inverted, so change flavor
+                                Flavor = 1;                 // from vanilla to chocolate
+              break;
+
+     case JCS_YCCK:            // Y/Cb/Cr/K
+              space = PT_CMYK;
+              Decompressor.out_color_space = JCS_CMYK;
+              if (Decompressor.saw_Adobe_marker)            // ditto
+                                Flavor = 1;
+              break;
+
+     default:
+              FatalError("Unsupported color space (0x%x)", Decompressor.jpeg_color_space);
+              return 0;
+     }
+
+     return (EXTRA_SH(extra)|CHANNELS_SH(ColorChannels)|BYTES_SH(bps)|COLORSPACE_SH(space)|FLAVOR_SH(Flavor));
+}
+
+
+// Rearrange pixel type to build output descriptor
+static
+cmsUInt32Number ComputeOutputFormatDescriptor(cmsUInt32Number dwInput, int OutColorSpace)
+{
+    int IsPlanar  = T_PLANAR(dwInput);
+    int Channels  = 0;
+    int Flavor    = 0;
+
+    switch (OutColorSpace) {
+
+   case PT_GRAY:
+       Channels = 1;
+       break;
+   case PT_RGB:
+   case PT_CMY:
+   case PT_Lab:
+   case PT_YUV:
+   case PT_YCbCr:
+       Channels = 3;
+       break;
+
+   case PT_CMYK:
+       if (Compressor.write_Adobe_marker)   // Adobe keeps CMYK inverted, so change flavor to chocolate
+           Flavor = 1;
+       Channels = 4;
+       break;
+   default:
+       FatalError("Unsupported output color space");
+    }
+
+    return (COLORSPACE_SH(OutColorSpace)|PLANAR_SH(IsPlanar)|CHANNELS_SH(Channels)|BYTES_SH(1)|FLAVOR_SH(Flavor));
+}
+
+
+// Equivalence between ICC color spaces and lcms color spaces
+static
+int GetProfileColorSpace(cmsContext ContextID, cmsHPROFILE hProfile)
+{
+    cmsColorSpaceSignature ProfileSpace = cmsGetColorSpace(ContextID, hProfile);
+
+    return _cmsLCMScolorSpace(ContextID, ProfileSpace);
+}
+
+static
+int GetDevicelinkColorSpace(cmsContext ContextID, cmsHPROFILE hProfile)
+{
+    cmsColorSpaceSignature ProfileSpace = cmsGetPCS(ContextID, hProfile);
+
+    return _cmsLCMScolorSpace(ContextID, ProfileSpace);
+}
+
+
+// From TRANSUPP
+
+static
+void jcopy_markers_execute(j_decompress_ptr srcinfo, j_compress_ptr dstinfo)
+{
+  jpeg_saved_marker_ptr marker;
+
+  /* In the current implementation, we don't actually need to examine the
+   * option flag here; we just copy everything that got saved.
+   * But to avoid confusion, we do not output JFIF and Adobe APP14 markers
+   * if the encoder library already wrote one.
+   */
+  for (marker = srcinfo->marker_list; marker != NULL; marker = marker->next) {
+
+    if (dstinfo->write_JFIF_header &&
+        marker->marker == JPEG_APP0 &&
+        marker->data_length >= 5 &&
+        GETJOCTET(marker->data[0]) == 0x4A &&
+        GETJOCTET(marker->data[1]) == 0x46 &&
+        GETJOCTET(marker->data[2]) == 0x49 &&
+        GETJOCTET(marker->data[3]) == 0x46 &&
+        GETJOCTET(marker->data[4]) == 0)
+                          continue;         /* reject duplicate JFIF */
+
+    if (dstinfo->write_Adobe_marker &&
+        marker->marker == JPEG_APP0+14 &&
+        marker->data_length >= 5 &&
+        GETJOCTET(marker->data[0]) == 0x41 &&
+        GETJOCTET(marker->data[1]) == 0x64 &&
+        GETJOCTET(marker->data[2]) == 0x6F &&
+        GETJOCTET(marker->data[3]) == 0x62 &&
+        GETJOCTET(marker->data[4]) == 0x65)
+                         continue;         /* reject duplicate Adobe */
+
+     jpeg_write_marker(dstinfo, marker->marker,
+                       marker->data, marker->data_length);
+  }
+}
+
+static
+void WriteOutputFields(int OutputColorSpace)
+{
+    J_COLOR_SPACE in_space, jpeg_space;
+    int components;
+
+    switch (OutputColorSpace) {
+
+    case PT_GRAY: in_space = jpeg_space = JCS_GRAYSCALE;
+                  components = 1;
+                  break;
+
+    case PT_RGB:  in_space = JCS_RGB;
+                  jpeg_space = JCS_YCbCr;
+                  components = 3;
+                  break;       // red/green/blue
+
+    case PT_YCbCr: in_space = jpeg_space = JCS_YCbCr;
+                   components = 3;
+                   break;               // Y/Cb/Cr (also known as YUV)
+
+    case PT_CMYK: in_space = JCS_CMYK;
+                  jpeg_space = JCS_YCCK;
+                  components = 4;
+                  break;      // C/M/Y/components
+
+    case PT_Lab:  in_space = jpeg_space = JCS_YCbCr;
+                  components = 3;
+                  break;                // Fake to don't touch
+    default:
+                 FatalError("Unsupported output color space");
+                 return;
+    }
+
+
+    if (jpegQuality >= 100) {
+
+     // avoid destructive conversion when asking for lossless compression
+        jpeg_space = in_space;
+    }
+
+    Compressor.in_color_space =  in_space;
+    Compressor.jpeg_color_space = jpeg_space;
+    Compressor.input_components = Compressor.num_components = components;
+    jpeg_set_defaults(&Compressor);
+    jpeg_set_colorspace(&Compressor, jpeg_space);
+
+
+    // Make sure to pass resolution through
+    if (OutputColorSpace == PT_CMYK)
+        Compressor.write_JFIF_header = 1;
+
+    // Avoid subsampling on high quality factor
+    jpeg_set_quality(&Compressor, jpegQuality, 1);
+    if (jpegQuality >= 70) {
+
+      int i;
+      for(i=0; i < Compressor.num_components; i++) {
+
+            Compressor.comp_info[i].h_samp_factor = 1;
+            Compressor.comp_info[i].v_samp_factor = 1;
+      }
+
+    }
+
+}
+
+
+static
+void DoEmbedProfile(const char* ProfileFile)
+{
+    FILE* f;
+    size_t size, EmbedLen;
+    cmsUInt8Number* EmbedBuffer;
+
+        f = fopen(ProfileFile, "rb");
+        if (f == NULL) return;
+
+        size = cmsfilelength(f);
+        EmbedBuffer = (cmsUInt8Number*) malloc(size + 1);
+        EmbedLen = fread(EmbedBuffer, 1, size, f);
+        fclose(f);
+        EmbedBuffer[EmbedLen] = 0;
+
+        write_icc_profile (&Compressor, EmbedBuffer, (unsigned int) EmbedLen);
+        free(EmbedBuffer);
+}
+
+
+
+static
+int DoTransform(cmsContext ContextID, cmsHTRANSFORM hXForm, int OutputColorSpace)
+{
+    JSAMPROW ScanLineIn;
+    JSAMPROW ScanLineOut;
+
+
+       //Preserve resolution values from the original
+       // (Thanks to Robert Bergs for finding out this bug)
+       Compressor.density_unit = Decompressor.density_unit;
+       Compressor.X_density    = Decompressor.X_density;
+       Compressor.Y_density    = Decompressor.Y_density;
+
+      //  Compressor.write_JFIF_header = 1;
+
+       jpeg_start_decompress(&Decompressor);
+       jpeg_start_compress(&Compressor, TRUE);
+
+        if (OutputColorSpace == PT_Lab)
+            SetITUFax(&Compressor);
+
+       // Embed the profile if needed
+       if (EmbedProfile && cOutProf)
+           DoEmbedProfile(cOutProf);
+
+       ScanLineIn  = (JSAMPROW) malloc((size_t) Decompressor.output_width * Decompressor.num_components);
+       ScanLineOut = (JSAMPROW) malloc((size_t) Compressor.image_width * Compressor.num_components);
+
+       while (Decompressor.output_scanline <
+                            Decompressor.output_height) {
+
+       jpeg_read_scanlines(&Decompressor, &ScanLineIn, 1);
+
+       cmsDoTransform(ContextID, hXForm, ScanLineIn, ScanLineOut, Decompressor.output_width);
+
+       jpeg_write_scanlines(&Compressor, &ScanLineOut, 1);
+       }
+
+       free(ScanLineIn);
+       free(ScanLineOut);
+
+       jpeg_finish_decompress(&Decompressor);
+       jpeg_finish_compress(&Compressor);
+
+       return TRUE;
+}
+
+
+
+// Transform one image
+
+static
+int TransformImage(cmsContext ContextID, char *cDefInpProf, char *cOutputProf)
+{
+       cmsHPROFILE hIn, hOut, hProof;
+       cmsHTRANSFORM xform;
+       cmsUInt32Number wInput, wOutput;
+       int OutputColorSpace;
+       cmsUInt32Number dwFlags = 0;
+       cmsUInt32Number EmbedLen;
+       cmsUInt8Number* EmbedBuffer;
+
+
+       cmsSetAdaptationState(ContextID, ObserverAdaptationState);
+
+       if (BlackPointCompensation) {
+
+            dwFlags |= cmsFLAGS_BLACKPOINTCOMPENSATION;
+       }
+
+
+       switch (PrecalcMode) {
+
+       case 0: dwFlags |= cmsFLAGS_NOOPTIMIZE; break;
+       case 2: dwFlags |= cmsFLAGS_HIGHRESPRECALC; break;
+       case 3: dwFlags |= cmsFLAGS_LOWRESPRECALC; break;
+       default:;
+       }
+
+
+       if (GamutCheck) {
+            dwFlags |= cmsFLAGS_GAMUTCHECK;
+            cmsSetAlarmCodes(ContextID, Alarm);
+       }
+
+       // Take input color space
+       wInput = GetInputPixelType();
+
+        if (lIsDeviceLink) {
+
+            hIn = cmsOpenProfileFromFile(ContextID, cDefInpProf, "r");
+            hOut = NULL;
+            hProof = NULL;
+       }
+        else {
+
+        if (!IgnoreEmbedded && read_icc_profile(&Decompressor, &EmbedBuffer, &EmbedLen))
+        {
+              hIn = cmsOpenProfileFromMem(ContextID, EmbedBuffer, EmbedLen);
+
+               if (Verbose) {
+
+                  fprintf(stdout, " (Embedded profile found)\n");
+                  PrintProfileInformation(ContextID, hIn);
+                  fflush(stdout);
+              }
+
+               if (hIn != NULL && SaveEmbedded != NULL)
+                          SaveMemoryBlock(EmbedBuffer, EmbedLen, SaveEmbedded);
+
+              free(EmbedBuffer);
+        }
+        else
+        {
+            // Default for ITU/Fax
+            if (cDefInpProf == NULL && T_COLORSPACE(wInput) == PT_Lab)
+                cDefInpProf = "*Lab";
+
+            if (cDefInpProf != NULL && cmsstrcasecmp(cDefInpProf, "*lab") == 0)
+                hIn = CreateITU2PCS_ICC();
+            else
+                hIn = OpenStockProfile(0, cDefInpProf);
+       }
+
+        if (cOutputProf != NULL && cmsstrcasecmp(cOutputProf, "*lab") == 0)
+            hOut = CreatePCS2ITU_ICC();
+        else
+        hOut = OpenStockProfile(0, cOutputProf);
+
+       hProof = NULL;
+       if (cProofing != NULL) {
+
+           hProof = OpenStockProfile(0, cProofing);
+           if (hProof == NULL) {
+            FatalError("Proofing profile couldn't be read.");
+           }
+           dwFlags |= cmsFLAGS_SOFTPROOFING;
+          }
+       }
+
+        if (!hIn)
+            FatalError("Input profile couldn't be read.");
+        if (!lIsDeviceLink && !hOut)
+            FatalError("Output profile couldn't be read.");
+
+       // Assure both, input profile and input JPEG are on same colorspace
+       if (cmsGetColorSpace(ContextID, hIn) != _cmsICCcolorSpace(ContextID, T_COLORSPACE(wInput)))
+              FatalError("Input profile is not operating in proper color space");
+
+
+       // Output colorspace is given by output profile
+
+        if (lIsDeviceLink) {
+            OutputColorSpace = GetDevicelinkColorSpace(ContextID, hIn);
+        }
+        else {
+            OutputColorSpace = GetProfileColorSpace(ContextID, hOut);
+        }
+
+       jpeg_copy_critical_parameters(&Decompressor, &Compressor);
+
+       WriteOutputFields(OutputColorSpace);
+
+       wOutput      = ComputeOutputFormatDescriptor(wInput, OutputColorSpace);
+
+
+       xform = cmsCreateProofingTransform(ContextID, hIn, wInput,
+                                          hOut, wOutput,
+                                          hProof, Intent,
+                                          ProofingIntent, dwFlags);
+       if (xform == NULL)
+                 FatalError("Cannot transform by using the profiles");
+
+       DoTransform(ContextID, xform, OutputColorSpace);
+
+
+       jcopy_markers_execute(&Decompressor, &Compressor);
+
+       cmsDeleteTransform(ContextID, xform);
+       cmsCloseProfile(ContextID, hIn);
+       cmsCloseProfile(ContextID, hOut);
+       if (hProof) cmsCloseProfile(ContextID, hProof);
+
+       return 1;
+}
+
+
+static
+void Help(cmsContext ContextID, int level)
+{
+
+    UTILS_UNUSED_PARAMETER(level);
+
+    fprintf(stderr, "usage: jpgicc [flags] input.jpg output.jpg\n");
+
+    fprintf(stderr, "\nflags:\n\n");
+    fprintf(stderr, "-v - Verbose\n");
+    fprintf(stderr, "-i<profile> - Input profile (defaults to sRGB)\n");
+    fprintf(stderr, "-o<profile> - Output profile (defaults to sRGB)\n");
+
+    PrintBuiltins();
+
+    PrintRenderingIntents(ContextID);
+
+
+    fprintf(stderr, "-b - Black point compensation\n");
+    fprintf(stderr, "-d<0..1> - Observer adaptation state (abs.col. only)\n");
+    fprintf(stderr, "-n - Ignore embedded profile\n");
+    fprintf(stderr, "-e - Embed destination profile\n");
+    fprintf(stderr, "-s<new profile> - Save embedded profile as <new profile>\n");
+
+    fprintf(stderr, "\n");
+
+    fprintf(stderr, "-c<0,1,2,3> - Precalculates transform (0=Off, 1=Normal, 2=Hi-res, 3=LoRes) [defaults to 1]\n");
+    fprintf(stderr, "\n");
+
+    fprintf(stderr, "-p<profile> - Soft proof profile\n");
+    fprintf(stderr, "-m<0,1,2,3> - SoftProof intent\n");
+    fprintf(stderr, "-g - Marks out-of-gamut colors on softproof\n");
+    fprintf(stderr, "-!<r>,<g>,<b> - Out-of-gamut marker channel values\n");
+
+    fprintf(stderr, "\n");
+    fprintf(stderr, "-q<0..100> - Output JPEG quality\n");
+
+    fprintf(stderr, "Examples:\n\n"
+        "To color correct from scanner to sRGB:\n"
+        "\tjpgicc -iscanner.icm in.jpg out.jpg\n"
+        "To convert from monitor1 to monitor2:\n"
+        "\tjpgicc -imon1.icm -omon2.icm in.jpg out.jpg\n"
+        "To make a CMYK separation:\n"
+        "\tjpgicc -oprinter.icm inrgb.jpg outcmyk.jpg\n"
+        "To recover sRGB from a CMYK separation:\n"
+        "\tjpgicc -iprinter.icm incmyk.jpg outrgb.jpg\n"
+        "To convert from CIELab ITU/Fax JPEG to sRGB\n"
+        "\tjpgicc in.jpg out.jpg\n\n");
+
+
+    fprintf(stderr, "This program is intended to be a demo of the Little CMS\n"
+        "color engine. Both lcms and this program are open source.\n"
+        "You can obtain both in source code at https://www.littlecms.com\n"
+        "For suggestions, comments, bug reports etc. send mail to\n"
+        "info@littlecms.com\n\n");
+
+    exit(0);
+}
+
+
+// The toggles stuff
+
+static
+void HandleSwitches(cmsContext ContextID, int argc, char *argv[])
+{
+    int s;
+
+    while ((s=xgetopt(argc,argv,"bBnNvVGgh:H:i:I:o:O:P:p:t:T:c:C:Q:q:M:m:L:l:eEs:S:!:D:d:-:")) != EOF) {
+
+        switch (s)
+        {
+
+        case '-':
+            if (strcmp(xoptarg, "help") == 0)
+            {
+                Help(ContextID, 0);
+            }
+            else
+            {
+                FatalError("Unknown option - run without args to see valid ones.\n");
+            }
+            break;
+
+        case 'b':
+        case 'B':
+            BlackPointCompensation = TRUE;
+            break;
+
+        case 'd':
+        case 'D': ObserverAdaptationState = atof(xoptarg);
+            if (ObserverAdaptationState < 0 ||
+                ObserverAdaptationState > 1.0)
+                FatalError("Adaptation state should be 0..1");
+            break;
+
+        case 'v':
+        case 'V':
+            Verbose = TRUE;
+            break;
+
+        case 'i':
+        case 'I':
+            if (lIsDeviceLink)
+                FatalError("Device-link already specified");
+
+            cInpProf = xoptarg;
+            break;
+
+        case 'o':
+        case 'O':
+            if (lIsDeviceLink)
+                FatalError("Device-link already specified");
+
+            cOutProf = xoptarg;
+            break;
+
+        case 'l':
+        case 'L':
+            if (cInpProf != NULL || cOutProf != NULL)
+                FatalError("input/output profiles already specified");
+
+            cInpProf = xoptarg;
+            lIsDeviceLink = TRUE;
+            break;
+
+        case 'p':
+        case 'P':
+            cProofing = xoptarg;
+            break;
+
+        case 't':
+        case 'T':
+            Intent = atoi(xoptarg);
+            break;
+
+        case 'N':
+        case 'n':
+            IgnoreEmbedded = TRUE;
+            break;
+
+        case 'e':
+        case 'E':
+            EmbedProfile = TRUE;
+            break;
+
+
+        case 'g':
+        case 'G':
+            GamutCheck = TRUE;
+            break;
+
+        case 'c':
+        case 'C':
+            PrecalcMode = atoi(xoptarg);
+            if (PrecalcMode < 0 || PrecalcMode > 2)
+                FatalError("Unknown precalc mode '%d'", PrecalcMode);
+            break;
+
+        case 'H':
+        case 'h':  {
+
+            int a =  atoi(xoptarg);
+            Help(ContextID, a);
+                   }
+            break;
+
+        case 'q':
+        case 'Q':
+            jpegQuality = atoi(xoptarg);
+            if (jpegQuality > 100) jpegQuality = 100;
+            if (jpegQuality < 0)   jpegQuality = 0;
+            break;
+
+        case 'm':
+        case 'M':
+            ProofingIntent = atoi(xoptarg);
+            break;
+
+        case 's':
+        case 'S': SaveEmbedded = xoptarg;
+            break;
+
+        case '!':
+            if (sscanf(xoptarg, "%hu,%hu,%hu", &Alarm[0], &Alarm[1], &Alarm[2]) == 3) {
+                int i;
+                for (i=0; i < 3; i++) {
+                    Alarm[i] = (Alarm[i] << 8) | Alarm[i];
+                }
+            }
+            break;
+
+        default:
+
+            FatalError("Unknown option - run without args to see valid ones");
+        }
+
+    }
+}
+
+
+int main(int argc, char* argv[])
+{
+    cmsContext ContextID = cmsCreateContext(NULL, NULL);
+
+    fprintf(stderr, "Little CMS ICC profile applier for JPEG - v3.4 [LittleCMS %2.2f]\n\n", cmsGetEncodedCMMversion() / 1000.0);
+    fprintf(stderr, "Copyright (c) 1998-2023 Marti Maria Saguer. See COPYING file for details.\n");
+    fflush(stderr);
+
+    InitUtils(ContextID, "jpgicc");
+
+    HandleSwitches(ContextID, argc, argv);
+
+    if ((argc - xoptind) != 2) {
+        Help(ContextID, 0);
+    }
+
+    OpenInput(argv[xoptind]);
+    OpenOutput(argv[xoptind+1]);
+
+    TransformImage(ContextID, cInpProf, cOutProf);
+
+
+    if (Verbose) { fprintf(stdout, "\n"); fflush(stdout); }
+
+    Done();
+
+    cmsDeleteContext(ContextID);
+
+    return 0;
+}