/*****************************************************************************/
// Copyright 2015-2023 Adobe Systems Incorporated
// All Rights Reserved.
//
// NOTICE:  Adobe permits you to use, modify, and distribute this file in
// accordance with the terms of the Adobe license agreement accompanying it.
/*****************************************************************************/

#include "dng_big_table.h"

#include "dng_1d_table.h"
#include "dng_bottlenecks.h"
#include "dng_abort_sniffer.h"
#include "dng_color_space.h"
#include "dng_globals.h"
#include "dng_host.h"
#include "dng_image.h"
#include "dng_image_writer.h"
#include "dng_info.h"
#include "dng_jxl.h"
#include "dng_memory_stream.h"
#include "dng_mutex.h"
#include "dng_negative.h"
#include "dng_stream.h"

#if qDNGUseXMP
#include "dng_xmp.h"
#endif

#include "zlib.h"

#include <unordered_map>

/*****************************************************************************/

class dng_big_table_cache
    {

    private:

        dng_std_mutex fMutex;

        typedef std::pair <dng_fingerprint,
                           int32> RefCountsPair;

        typedef std::map <dng_fingerprint,
                          int32,
                          dng_fingerprint_less_than> RefCountsMap;

        RefCountsMap fRefCounts;

        std::vector<dng_fingerprint> fRecentlyUsed;

    protected:

        enum
            {
            kDefaultRecentlyUsedLimit = 5
            };

        uint32 fRecentlyUsedLimit;

    protected:

        dng_big_table_cache ();

        void UseTable (dng_lock_std_mutex &lock,
                       const dng_fingerprint &fingerprint);

        virtual void CacheIncrement (dng_lock_std_mutex &lock,
                                     const dng_fingerprint &fingerprint);

        virtual void CacheDecrement (dng_lock_std_mutex &lock,
                                     const dng_fingerprint &fingerprint);

        virtual void CacheAdd (dng_lock_std_mutex &lock,
                               const dng_big_table &table);

        virtual bool CacheExtract (dng_lock_std_mutex &lock,
                                   const dng_fingerprint &fingerprint,
                                   dng_big_table &table);

        virtual void InsertTableData (dng_lock_std_mutex &lock,
                                      const dng_big_table &table) = 0;

        virtual void EraseTableData (dng_lock_std_mutex &lock,
                                     const dng_fingerprint &fingerprint) = 0;

        virtual void ExtractTableData (dng_lock_std_mutex &lock,
                                       const dng_fingerprint &fingerprint,
                                       dng_big_table &table) = 0;

        virtual void Clear ()
            {

            fRefCounts.clear ();

            fRecentlyUsed.clear ();

            }

    public:

        virtual ~dng_big_table_cache ();

        void FlushRecentlyUsed ();

        static void Increment (dng_big_table_cache *cache,
                               const dng_fingerprint &fingerprint);

        static void Decrement (dng_big_table_cache *cache,
                               const dng_fingerprint &fingerprint);

        static void Add (dng_big_table_cache *cache,
                         const dng_big_table &table);

        static bool Extract (dng_big_table_cache *cache,
                             const dng_fingerprint &fingerprint,
                             dng_big_table &table);

    };

/*****************************************************************************/

dng_big_table_cache::dng_big_table_cache ()

    :   fMutex ()

    ,   fRefCounts ()

    ,   fRecentlyUsed      ()
    ,   fRecentlyUsedLimit (kDefaultRecentlyUsedLimit)

    {

    }

/*****************************************************************************/

dng_big_table_cache::~dng_big_table_cache ()
    {

    }

/*****************************************************************************/

void dng_big_table_cache::UseTable (dng_lock_std_mutex &lock,
                                    const dng_fingerprint &fingerprint)
    {

    // See if fingerprint is in recently used list.

    int32 lastIndex = (int32) fRecentlyUsed.size () - 1;

    for (int32 index = lastIndex; index >= 0; index--)
        {

        if (fingerprint == fRecentlyUsed [index])
            {

            // Move to end of list if not already there.

            if (index != lastIndex)
                {

                fRecentlyUsed.erase (fRecentlyUsed.begin () + index);

                fRecentlyUsed.push_back (fingerprint);

                }

            // Item is in recently used list, so we are done.

            return;

            }

        }

    // Is the recently used list full?

    if (fRecentlyUsed.size () == fRecentlyUsedLimit)
        {

        CacheDecrement (lock, fRecentlyUsed.front ());

        fRecentlyUsed.erase (fRecentlyUsed.begin ());

        }

    // Add to end of list.

    fRecentlyUsed.push_back (fingerprint);

    CacheIncrement (lock, fingerprint);

    }

/*****************************************************************************/

void dng_big_table_cache::FlushRecentlyUsed ()
    {

    dng_lock_std_mutex lock (fMutex);

    while (!fRecentlyUsed.empty ())
        {

        CacheDecrement (lock, fRecentlyUsed.back ());

        fRecentlyUsed.pop_back ();

        }

    }

/*****************************************************************************/

void dng_big_table_cache::CacheIncrement (dng_lock_std_mutex &lock,
                                          const dng_fingerprint &fingerprint)
    {

    if (fingerprint.IsValid ())
        {

        RefCountsMap::iterator it = fRefCounts.find (fingerprint);

        if (it == fRefCounts.end ())
            {

            DNG_REPORT ("dng_big_table_cache::CacheIncrement"
                        "fingerprint not in cache");

            }

        else
            {

            it->second++;

            UseTable (lock, fingerprint);

            }

        }

    }

/*****************************************************************************/

void dng_big_table_cache::CacheDecrement (dng_lock_std_mutex &lock,
                                          const dng_fingerprint &fingerprint)
    {

    if (fingerprint.IsValid ())
        {

        RefCountsMap::iterator it = fRefCounts.find (fingerprint);

        if (it == fRefCounts.end ())
            {

            DNG_REPORT ("dng_big_table_cache::CacheDecrement"
                        "fingerprint not in cache");

            }

        else if (--(it->second) == 0)
            {

            fRefCounts.erase (it);

            EraseTableData (lock, fingerprint);

            }

        }

    }

/*****************************************************************************/

void dng_big_table_cache::CacheAdd (dng_lock_std_mutex &lock,
                                    const dng_big_table &table)
    {

    if (table.Fingerprint ().IsValid ())
        {

        RefCountsMap::iterator it = fRefCounts.find (table.Fingerprint ());

        if (it == fRefCounts.end ())
            {

            fRefCounts.insert (RefCountsPair (table.Fingerprint (), 1));

            InsertTableData (lock, table);

            }

        else
            {

            it->second++;

            }

        UseTable (lock, table.Fingerprint ());

        }

    }

/*****************************************************************************/

bool dng_big_table_cache::CacheExtract (dng_lock_std_mutex &lock,
                                        const dng_fingerprint &fingerprint,
                                        dng_big_table &table)
    {

    if (fingerprint.IsValid ())
        {

        RefCountsMap::iterator it = fRefCounts.find (fingerprint);

        if (it != fRefCounts.end ())
            {

            it->second++;

            ExtractTableData (lock, fingerprint, table);

            UseTable (lock, fingerprint);

            return true;

            }

        }

    return false;

    }

/*****************************************************************************/

void dng_big_table_cache::Increment (dng_big_table_cache *cache,
                                     const dng_fingerprint &fingerprint)
    {

    if (cache)
        {

        dng_lock_std_mutex lock (cache->fMutex);

        cache->CacheIncrement (lock, fingerprint);

        }

    }

/*****************************************************************************/

void dng_big_table_cache::Decrement (dng_big_table_cache *cache,
                                     const dng_fingerprint &fingerprint)
    {

    if (cache)
        {

        dng_lock_std_mutex lock (cache->fMutex);

        cache->CacheDecrement (lock, fingerprint);

        }

    }

/*****************************************************************************/

void dng_big_table_cache::Add (dng_big_table_cache *cache,
                               const dng_big_table &table)
    {

    if (cache)
        {

        dng_lock_std_mutex lock (cache->fMutex);

        cache->CacheAdd (lock, table);

        }

    }

/*****************************************************************************/

bool dng_big_table_cache::Extract (dng_big_table_cache *cache,
                                   const dng_fingerprint &fingerprint,
                                   dng_big_table &table)
    {

    if (cache)
        {

        dng_lock_std_mutex lock (cache->fMutex);

        return cache->CacheExtract (lock, fingerprint, table);

        }

    return false;

    }

/*****************************************************************************/

class dng_look_table_cache : public dng_big_table_cache
    {

    private:

        typedef std::pair <dng_fingerprint,
                           dng_look_table::table_data> TableDataPair;

        typedef std::map <dng_fingerprint,
                          dng_look_table::table_data,
                          dng_fingerprint_less_than> TableDataMap;

        TableDataMap fTableData;

    public:

        dng_look_table_cache ()

            :   fTableData ()

            {
            }

        virtual void Clear ()
            {

            dng_big_table_cache::Clear ();

            fTableData.clear ();

            }

        virtual void InsertTableData (dng_lock_std_mutex & /* lock */,
                                      const dng_big_table &table)
            {

            const dng_look_table *lookTable = static_cast
                                              <const dng_look_table *>
                                              (&table);

            fTableData.insert (TableDataPair (lookTable->Fingerprint (),
                                              lookTable->fData));

            }

        virtual void EraseTableData (dng_lock_std_mutex & /* lock */,
                                     const dng_fingerprint &fingerprint)
            {

            TableDataMap::iterator it = fTableData.find (fingerprint);

            if (it != fTableData.end ())
                {

                fTableData.erase (it);

                }

            else
                {

                DNG_REPORT ("dng_look_table_cache::EraseTableData"
                            "fingerprint not in cache");

                }

            }

        virtual void ExtractTableData (dng_lock_std_mutex & /* lock */,
                                       const dng_fingerprint &fingerprint,
                                       dng_big_table &table)
            {

            TableDataMap::iterator it = fTableData.find (fingerprint);

            if (it != fTableData.end ())
                {

                dng_look_table *lookTable = static_cast
                                            <dng_look_table *>
                                            (&table);

                lookTable->fData = it->second;

                }

            else
                {

                DNG_REPORT ("dng_look_table_cache::ExtractTableData"
                            "fingerprint not in cache");

                }

            }

    };

static dng_look_table_cache gLookTableCache;

/*****************************************************************************/

class dng_rgb_table_cache : public dng_big_table_cache
    {

    private:

        typedef std::pair <dng_fingerprint,
                           dng_rgb_table::table_data> TableDataPair;

        typedef std::map <dng_fingerprint,
                          dng_rgb_table::table_data,
                          dng_fingerprint_less_than> TableDataMap;

        TableDataMap fTableData;

    public:

        dng_rgb_table_cache ()

            :   fTableData ()

            {
            }

        virtual void Clear ()
            {

            dng_big_table_cache::Clear ();

            fTableData.clear ();

            }

        virtual void InsertTableData (dng_lock_std_mutex & /* lock */,
                                      const dng_big_table &table)
            {

            const dng_rgb_table *rgbTable = static_cast
                                            <const dng_rgb_table *>
                                            (&table);

            fTableData.insert (TableDataPair (rgbTable->Fingerprint (),
                                              rgbTable->fData));

            }

        virtual void EraseTableData (dng_lock_std_mutex & /* lock */,
                                     const dng_fingerprint &fingerprint)
            {

            TableDataMap::iterator it = fTableData.find (fingerprint);

            if (it != fTableData.end ())
                {

                fTableData.erase (it);

                }

            else
                {

                DNG_REPORT ("dng_rgb_table_cache::EraseTableData"
                            "fingerprint not in cache");

                }

            }

        virtual void ExtractTableData (dng_lock_std_mutex & /* lock */,
                                       const dng_fingerprint &fingerprint,
                                       dng_big_table &table)
            {

            TableDataMap::iterator it = fTableData.find (fingerprint);

            if (it != fTableData.end ())
                {

                dng_rgb_table *rgbTable = static_cast
                                          <dng_rgb_table *>
                                          (&table);

                rgbTable->fData = it->second;

                }

            else
                {

                DNG_REPORT ("dng_rgb_table_cache::ExtractTableData"
                            "fingerprint not in cache");

                }

            }

    };

static dng_rgb_table_cache gRGBTableCache;

/*****************************************************************************/

class dng_image_table_cache : public dng_big_table_cache
    {

    private:

        std::unordered_map <dng_fingerprint,
                            dng_image_table_data,
                            dng_fingerprint_hash> fTableData;

    public:

        void Clear () override
            {

            dng_big_table_cache::Clear ();

            fTableData.clear ();

            }

        void InsertTableData (dng_lock_std_mutex & /* lock */,
                              const dng_big_table &table) override
            {

            const dng_image_table *imageTable = static_cast
                                                <const dng_image_table *>
                                                (&table);

            dng_image_table_data data;

            imageTable->GetData (data);

            fTableData.insert (std::make_pair (imageTable->Fingerprint (),
                                               data));

            }

        void EraseTableData (dng_lock_std_mutex & /* lock */,
                             const dng_fingerprint &fingerprint) override
            {

            auto it = fTableData.find (fingerprint);

            if (it != fTableData.end ())
                {

                fTableData.erase (it);

                }

            else
                {

                DNG_REPORT ("dng_image_table_cache::EraseTableData"
                            "fingerprint not in cache");

                }

            }

        void ExtractTableData (dng_lock_std_mutex & /* lock */,
                               const dng_fingerprint &fingerprint,
                               dng_big_table &table) override
            {

            auto it = fTableData.find (fingerprint);

            if (it != fTableData.end ())
                {

                dng_image_table *imageTable = static_cast
                                              <dng_image_table *>
                                              (&table);

                imageTable->SetData (it->second);

                }

            else
                {

                DNG_REPORT ("dng_image_table_cache::ExtractTableData"
                            "fingerprint not in cache");

                }

            }

    };

static dng_image_table_cache gImageTableCache;

/*****************************************************************************/

class dng_packed_image_table_cache : public dng_big_table_cache
    {

    private:

        struct entry_t
            {

            public:

                dng_image_table_data fImageData;

                std::shared_ptr<const dng_memory_block> fBlock;

                // Image properties.

                dng_point fSize;

                uint32 fPlanes = 0;

                uint32 fPixelType = 0;

            };

        std::unordered_map <dng_fingerprint,
                            entry_t,
                            dng_fingerprint_hash> fEntries;

    public:

        void Clear () override
            {

            dng_big_table_cache::Clear ();

            fEntries.clear ();

            }

         void InsertTableData (dng_lock_std_mutex & /* lock */,
                               const dng_big_table &table) override
            {

            // printf ("dng_packed_image_table_cache::InsertTableData\n");

            const auto &src = static_cast <const dng_packed_image_table &> (table);

            entry_t entry;

            entry.fBlock     = src.ShareBlock ();
            entry.fSize      = src.fSize;
            entry.fPlanes    = src.fPlanes;
            entry.fPixelType = src.fPixelType;

            if (src.IsValidUnpacked ())
                src.Table ().GetData (entry.fImageData);

            fEntries.insert (std::make_pair (src.Fingerprint (), entry));

            }

         void EraseTableData (dng_lock_std_mutex & /* lock */,
                              const dng_fingerprint &fingerprint) override
            {

            // printf ("dng_packed_image_table_cache::EraseTableData\n");

            auto iter = fEntries.find (fingerprint);

            if (iter != fEntries.end ())
                {

                fEntries.erase (iter);

                }

            else
                {

                DNG_REPORT ("dng_packed_image_table_cache::EraseTableData"
                            "fingerprint not in cache");

                }

            }

         void ExtractTableData (dng_lock_std_mutex & /* lock */,
                                const dng_fingerprint &fingerprint,
                                dng_big_table &table) override
            {

            // printf ("dng_packed_image_table_cache::ExtractTableData\n");

            auto iter = fEntries.find (fingerprint);

            if (iter != fEntries.end ())
                {

                auto &dst = static_cast <dng_packed_image_table &> (table);

                dst.fTableDigest = fingerprint;

                dst.fBlock     = iter->second.fBlock;
                dst.fSize      = iter->second.fSize;
                dst.fPlanes    = iter->second.fPlanes;
                dst.fPixelType = iter->second.fPixelType;

                AutoPtr<dng_image_table> temp (dst.MakeTable ());

                temp->SetData (iter->second.fImageData);

                dst.fTable.reset (temp.Release ());

                }

            else
                {

                DNG_REPORT ("dng_packed_image_table_cache::ExtractTableData"
                            "fingerprint not in cache");

                }

            }

    };

static dng_packed_image_table_cache gPackedImageTableCache;

/*****************************************************************************/

void dng_big_table_cache_flush ()
    {

    gLookTableCache.FlushRecentlyUsed ();

    gRGBTableCache.FlushRecentlyUsed ();

    gImageTableCache.FlushRecentlyUsed ();

    gPackedImageTableCache.FlushRecentlyUsed ();

    }

/*****************************************************************************/

void dng_big_table_cache_clear ()
    {

    gLookTableCache.Clear ();

    gRGBTableCache.Clear ();

    gImageTableCache.Clear ();

    gPackedImageTableCache.Clear ();

    }

/*****************************************************************************/

dng_big_table::dng_big_table (dng_big_table_cache *cache)

    :   fFingerprint ()
    ,   fCache       (cache)
    ,   fIsMissing   (false)

    {

    }

/*****************************************************************************/

dng_big_table::dng_big_table (const dng_big_table &table)

    :   fFingerprint (table.fFingerprint)
    ,   fCache       (table.fCache      )
    ,   fIsMissing   (false             )

    {

    dng_big_table_cache::Increment (fCache, fFingerprint);

    }

/*****************************************************************************/

dng_big_table & dng_big_table::operator= (const dng_big_table &table)
    {

    if (fFingerprint != table.fFingerprint ||
        fCache       != table.fCache       )
        {

        dng_big_table_cache::Decrement (fCache, fFingerprint);

        fFingerprint = table.fFingerprint;
        fCache       = table.fCache;

        dng_big_table_cache::Increment (fCache, fFingerprint);

        }

    return *this;

    }

/*****************************************************************************/

dng_big_table::~dng_big_table ()
    {

    dng_big_table_cache::Decrement (fCache, fFingerprint);

    }

/*****************************************************************************/

const dng_fingerprint & dng_big_table::Fingerprint () const
    {

    return fFingerprint;

    }

/*****************************************************************************/

dng_fingerprint dng_big_table::ComputeFingerprint () const
    {

    dng_md5_printer_stream stream;

    stream.SetLittleEndian ();

    PutStream (stream, true);

    return stream.Result ();

    }

/*****************************************************************************/

void dng_big_table::RecomputeFingerprint ()
    {

    dng_big_table_cache::Decrement (fCache, fFingerprint);

    fFingerprint.Clear ();

    if (IsValid ())
        {

        fFingerprint = ComputeFingerprint ();

        // Try extract first to force sharing of table data memory if
        // table data is already in cache.

        if (!dng_big_table_cache::Extract (fCache, fFingerprint, *this))
            {

            // Otherwise add to cache.

            dng_big_table_cache::Add (fCache, *this);

            }

        }

    }

/*****************************************************************************/

bool dng_big_table::DecodeFromBinary (dng_host &host,
                                      const uint8 * compressedData,
                                      uint32 compressedSize,
                                      AutoPtr<dng_memory_block> *uncompressedCache)
    {

    // Decompress the data, if required.

    if (UseCompression ())
        {

        if (compressedSize < 5)
            {
            return false;
            }

        uint8 *uncompressedData;
        uint32 uncompressedSize;

        AutoPtr<dng_memory_block> uncompressedBlock;

        if (uncompressedCache && uncompressedCache->Get ())
            {
            uncompressedData = uncompressedCache->Get ()->Buffer_uint8 ();
            uncompressedSize = uncompressedCache->Get ()->LogicalSize  ();
            }

        else
            {

            // Uncompressed size is stored in first four bytes of decoded data,
            // little endian order.

            uncompressedSize = (((uint32) compressedData [0])      ) +
                               (((uint32) compressedData [1]) <<  8) +
                               (((uint32) compressedData [2]) << 16) +
                               (((uint32) compressedData [3]) << 24);

            uncompressedBlock.Reset (host.Allocate (uncompressedSize));

            uncompressedData = uncompressedBlock->Buffer_uint8 ();

            uLongf destLen = uncompressedSize;

            int zResult = ::uncompress (uncompressedBlock->Buffer_uint8 (),
                                        &destLen,
                                        compressedData + 4,
                                        compressedSize - 4);

            if (zResult != Z_OK)
                {
                return false;
                }

            if (uncompressedCache)
                {
                uncompressedCache->Reset (uncompressedBlock.Release ());
                }

            }

        // Now read in the table data from the uncompressed stream.

        try
            {

            dng_stream stream (uncompressedData,
                               uncompressedSize);

            stream.SetLittleEndian ();

            stream.SetSniffer (host.Sniffer ());

            if (!GetStream (stream))
                {
                return false;
                }

            }

        catch (dng_exception &except)
            {

            if (host.IsTransientError (except.ErrorCode ()))
                {
                throw;
                }

            return false;

            }

        catch (...)
            {

            return false;

            }

        }

    // Simple uncompressed stream for this big table class.

    else
        {

        try
            {

            dng_stream stream (compressedData,
                               compressedSize);

            stream.SetLittleEndian ();

            stream.SetSniffer (host.Sniffer ());

            if (!GetStream (stream))
                {
                return false;
                }

            }

        catch (dng_exception &except)
            {

            if (host.IsTransientError (except.ErrorCode ()))
                {
                throw;
                }

            return false;

            }

        catch (...)
            {

            return false;

            }

        }

    // Force recomputation of fingerprint.

    RecomputeFingerprint ();

    return true;

    }

/*****************************************************************************/

void dng_big_table::ASCIItoBinary (dng_memory_allocator &allocator,
                                   const char *sPtr,
                                   uint32 sCount,
                                   AutoPtr<dng_memory_block> &dBlock,
                                   uint32 &dCount)
    {

    // This binary to text encoding is very similar to the Z85
    // encoding, but the exact character set has been adjusted to
    // encode more cleanly into XMP.

    static uint8 kDecodeTable [96] =
        {

        0xFF,   // space
        0x44,   // !
        0xFF,   // "
        0x54,   // #
        0x53,   // $
        0x52,   // %
        0xFF,   // &
        0x49,   // '
        0x4B,   // (
        0x4C,   // )
        0x46,   // *
        0x41,   // +
        0xFF,   // ,
        0x3F,   // -
        0x3E,   // .
        0x45,   // /

        0x00, 0x01, 0x02, 0x03, 0x04,   // 01234
        0x05, 0x06, 0x07, 0x08, 0x09,   // 56789

        0x40,   // :
        0xFF,   // ;
        0xFF,   // <
        0x42,   // =
        0xFF,   // >
        0x47,   // ?
        0x51,   // @

        0x24, 0x25, 0x26, 0x27, 0x28,   // ABCDE
        0x29, 0x2A, 0x2B, 0x2C, 0x2D,   // FGHIJ
        0x2E, 0x2F, 0x30, 0x31, 0x32,   // KLMNO
        0x33, 0x34, 0x35, 0x36, 0x37,   // PQRST
        0x38, 0x39, 0x3A, 0x3B, 0x3C,   // UVWXY
        0x3D,                           // Z

        0x4D,   // [
        0xFF,   // backslash
        0x4E,   // ]
        0x43,   // ^
        0xFF,   // _
        0x48,   // `

        0x0A, 0x0B, 0x0C, 0x0D, 0x0E,   // abcde
        0x0F, 0x10, 0x11, 0x12, 0x13,   // fghij
        0x14, 0x15, 0x16, 0x17, 0x18,   // klmno
        0x19, 0x1A, 0x1B, 0x1C, 0x1D,   // pqrst
        0x1E, 0x1F, 0x20, 0x21, 0x22,   // uvwxy
        0x23,                           // z

        0x4F,   // {
        0x4A,   // |
        0x50,   // }
        0xFF,   // ~
        0xFF    // del

        };

    dCount = 0;

    uint32 maxDecodedSize = (sCount + 4) / 5 * 4;

    dBlock.Reset (allocator.Allocate (maxDecodedSize));

    uint32 phase = 0;
    uint32 value;

    uint8 *dPtr = dBlock->Buffer_uint8 ();

    for (uint32 j = 0; j < sCount; j++)
        {

        uint8 e = (uint8) sPtr [j];

        if (e < 32 || e > 127)
            {
            continue;
            }

        uint32 d = kDecodeTable [e - 32];

        if (d > 85)
            {
            continue;
            }

        phase++;

        if (phase == 1)
            {
            value = d;
            }

        else if (phase == 2)
            {
            value += d * 85;
            }

        else if (phase == 3)
            {
            value += d * (85 * 85);
            }

        else if (phase == 4)
            {
            value += d * (85 * 85 * 85);
            }

        else
            {

            value += d * (85 * 85 * 85 * 85);

            dPtr [0] = (uint8) (value      );
            dPtr [1] = (uint8) (value >>  8);
            dPtr [2] = (uint8) (value >> 16);
            dPtr [3] = (uint8) (value >> 24);

            dPtr += 4;

            dCount += 4;

            phase = 0;

            }

        }

    if (phase > 3)
        {

        dPtr [2] = (uint8) (value >> 16);

        dCount++;

        }

    if (phase > 2)
        {

        dPtr [1] = (uint8) (value >> 8);

        dCount++;

        }

    if (phase > 1)
        {

        dPtr [0] = (uint8) (value);

        dCount++;

        }

    }

/*****************************************************************************/

bool dng_big_table::DecodeFromString (dng_host &host,
                                      const dng_string &block1)
    {

    // Decode the text to binary.

    AutoPtr<dng_memory_block> block2;

    uint32 compressedSize = 0;

    ASCIItoBinary (host.Allocator (),
                   block1.Get (),
                   block1.Length (),
                   block2,
                   compressedSize);

    // Then decode table from the binary data.

    return DecodeFromBinary (host,
                             block2->Buffer_uint8 (),
                             compressedSize);

    }

/*****************************************************************************/

dng_memory_block* dng_big_table::EncodeAsBinary (dng_memory_allocator &allocator,
                                                 uint32 &compressedSize) const
    {

    // Stream to a binary block.

    AutoPtr<dng_memory_block> block1;

        {

        dng_memory_stream stream (allocator);

        stream.SetLittleEndian ();

        PutStream (stream, false);

        block1.Reset (stream.AsMemoryBlock (allocator));

        }

    // If we are not compressing this type, we are done.

    if (!UseCompression ())
        {

        compressedSize = block1->LogicalSize ();

        return block1.Release ();

        }

    // Compress the block.

    AutoPtr<dng_memory_block> block2;

        {

        uint32 uncompressedSize = block1->LogicalSize ();

        uint32 safeCompressedSize = uncompressedSize + (uncompressedSize >> 8) + 64;

        block2.Reset (allocator.Allocate (safeCompressedSize + 4));

        // Store uncompressed size in first four bytes of compressed block.

        uint8 *dPtr = block2->Buffer_uint8 ();

        dPtr [0] = (uint8) (uncompressedSize      );
        dPtr [1] = (uint8) (uncompressedSize >>  8);
        dPtr [2] = (uint8) (uncompressedSize >> 16);
        dPtr [3] = (uint8) (uncompressedSize >> 24);

        uLongf dCount = safeCompressedSize;

        int zResult = ::compress2 (dPtr + 4,
                                   &dCount,
                                   block1->Buffer_uint8 (),
                                   uncompressedSize,
                                   Z_DEFAULT_COMPRESSION);

        if (zResult != Z_OK)
            {
            ThrowMemoryFull ();
            }

        compressedSize = (uint32) dCount + 4;

        block1.Reset ();

        }

    return block2.Release ();

    }

/*****************************************************************************/

dng_memory_block* dng_big_table::EncodeAsString (dng_memory_allocator &allocator) const
    {

    // Get compressed binary data.

    uint32 compressedSize;

    AutoPtr<dng_memory_block> block2 (EncodeAsBinary (allocator, compressedSize));

    // Encode binary to text.

    AutoPtr<dng_memory_block> block3;

        {

        // This binary to text encoding is very similar to the Z85
        // encoding, but the exact character set has been adjusted to
        // encode more cleanly into XMP.

        static const char *kEncodeTable =
            "0123456789"
            "abcdefghij"
            "klmnopqrst"
            "uvwxyzABCD"
            "EFGHIJKLMN"
            "OPQRSTUVWX"
            "YZ.-:+=^!/"
            "*?`'|()[]{"
            "}@%$#";

        uint32 safeEncodedSize = compressedSize +
                                 (compressedSize >> 2) +
                                 (compressedSize >> 6) +
                                 16;

        block3.Reset (allocator.Allocate (safeEncodedSize));

        uint8 *sPtr = block2->Buffer_uint8 ();

        sPtr [compressedSize    ] = 0;
        sPtr [compressedSize + 1] = 0;
        sPtr [compressedSize + 2] = 0;

        uint8 *dPtr = block3->Buffer_uint8 ();

        while (compressedSize)
            {

            uint32 x0 = (((uint32) sPtr [0])      ) +
                        (((uint32) sPtr [1]) <<  8) +
                        (((uint32) sPtr [2]) << 16) +
                        (((uint32) sPtr [3]) << 24);

            sPtr += 4;

            uint32 x1 = x0 / 85;

            *dPtr++ = kEncodeTable [x0 - x1 * 85];

            uint32 x2 = x1 / 85;

            *dPtr++ = kEncodeTable [x1 - x2 * 85];

            if (!--compressedSize)
                break;

            uint32 x3 = x2 / 85;

            *dPtr++ = kEncodeTable [x2 - x3 * 85];

            if (!--compressedSize)
                break;

            uint32 x4 = x3 / 85;

            *dPtr++ = kEncodeTable [x3 - x4 * 85];

            if (!--compressedSize)
                break;

            *dPtr++ = kEncodeTable [x4];

            compressedSize--;

            }

        *dPtr = 0;

        block2.Reset ();

        }

    return block3.Release ();

    }

/*****************************************************************************/

bool dng_big_table::ExtractFromCache (const dng_fingerprint &fingerprint)
    {

    if (dng_big_table_cache::Extract (fCache, fingerprint, *this))
        {

        fFingerprint = fingerprint;

        return true;

        }

    return false;

    }

/*****************************************************************************/

#if qDNGUseXMP

/*****************************************************************************/

bool dng_big_table::ReadTableFromXMP (const dng_xmp &xmp,
                                      const char *ns,
                                      const dng_fingerprint &fingerprint,
                                      dng_big_table_storage *storage,
                                      dng_abort_sniffer *sniffer)
    {

    // See if we can skip reading the table data, and just grab from cache.

    if (ExtractFromCache (fingerprint))
        {

        return true;

        }

    // Next see if we can get the table from the storage object.

    if (storage && storage->ReadTable (*this, fingerprint, xmp.Allocator ()))
        {

        return true;

        }

    // Not in cache nor storage, so we need to read from XMP.

    dng_host host (&xmp.Allocator (), sniffer);

    host.SniffForAbort ();

    dng_string tablePath;

    tablePath.Set ("Table_");

    tablePath.Append (dng_xmp::EncodeFingerprint (fingerprint).Get ());

    dng_string block1;

    if (!xmp.GetString (ns,
                        tablePath.Get (),
                        block1))
        {

        DNG_REPORT ("Missing big table data");

        return false;

        }

    host.SniffForAbort ();

    bool ok = DecodeFromString (host, block1);

    block1.Clear ();

    host.SniffForAbort ();

    // Validate fingerprint match. Only bother doing this if decoding
    // succeeded, otherwise we expect the fingerprint to be wrong and the
    // assert message is not helpful.

    if (ok)
        {

        DNG_ASSERT (Fingerprint () == fingerprint,
                    "dng_big_table fingerprint mismatch");

        }

    // It worked!

    return ok;

    }

/*****************************************************************************/

bool dng_big_table::ReadFromXMP (const dng_xmp &xmp,
                                 const char *ns,
                                 const char *path,
                                 dng_big_table_storage &storage,
                                 dng_abort_sniffer *sniffer)
    {

    dng_fingerprint fingerprint;

    if (!xmp.GetFingerprint (ns, path, fingerprint))
        {

        return false;

        }

    // See if we can skip reading the table data, and just grab from cache.

    if (ExtractFromCache (fingerprint))
        {

        return true;

        }

    // Next see if we can get the table from the storage object.

    if (storage.ReadTable (*this, fingerprint, xmp.Allocator ()))
        {

        return true;

        }

    // Read in the table data. We already checked the storage object
    // (above), so pass in nullptr for 4th argument.

    if (ReadTableFromXMP (xmp, ns, fingerprint, nullptr, sniffer))
        {

        return true;

        }

    // Unable to find table data anywhere.  Notify storage object.

    storage.MissingTable (fingerprint);

    // Also make a note that this table is missing.

    SetMissing ();

    return false;

    }

/*****************************************************************************/

void dng_big_table::WriteToXMP (dng_xmp &xmp,
                                const char *ns,
                                const char *path,
                                dng_big_table_storage &storage) const
    {

    const dng_fingerprint &fingerprint = Fingerprint ();

    if (!fingerprint.IsValid () || IsMissing ())
        {

        xmp.Remove (ns, path);

        return;

        }

    xmp.SetFingerprint (ns, path, fingerprint);

    // See if we can just use the storage object to store the table.

    if (storage.WriteTable (*this, fingerprint, xmp.Allocator ()))
        {

        return;

        }

    dng_string tablePath;

    tablePath.Set ("Table_");

    tablePath.Append (dng_xmp::EncodeFingerprint (fingerprint).Get ());

    if (xmp.Exists (ns, tablePath.Get ()))
        {

        return;

        }

    AutoPtr<dng_memory_block> block;

    block.Reset (EncodeAsString (xmp.Allocator ()));

    xmp.Set (ns,
             tablePath.Get (),
             block->Buffer_char ());

    }

/*****************************************************************************/

#endif  // qDNGUseXMP

/*****************************************************************************/

#if qDNGValidate

void dng_big_table::WriteUncompressedStream (dng_stream &stream) const
    {

    stream.SetLittleEndian ();

    PutStream (stream, false);

    }

#endif  // qDNGValidate

/*****************************************************************************/

bool dng_big_table_dictionary::HasTable (const dng_fingerprint &fingerprint) const
    {

    if (fMap.find (fingerprint) != fMap.end ())
        {

        return true;

        }

    return false;

    }

/*****************************************************************************/

bool dng_big_table_dictionary::GetTable (const dng_fingerprint &fingerprint,
                                         dng_ref_counted_block &block) const
    {

    const auto it = fMap.find (fingerprint);

    if (it != fMap.end ())
        {

        block = it->second;

        return true;

        }

    return false;

    }

/*****************************************************************************/

void dng_big_table_dictionary::AddTable (const dng_fingerprint &fingerprint,
                                         const dng_ref_counted_block &block)
    {

    if (fMap.find (fingerprint) == fMap.end ())
        {

        fMap.insert (std::pair<dng_fingerprint,
                               dng_ref_counted_block> (fingerprint,
                                                       block));

        }

    }

/*****************************************************************************/

void dng_big_table_dictionary::CopyToDictionary
                               (dng_big_table_dictionary &dictionary) const

    {

    for (auto it = fMap.cbegin (); it != fMap.cend (); ++it)
        {

        dictionary.AddTable (it->first,
                             it->second);

        }

    }

/*****************************************************************************/

dng_big_table_index::dng_big_table_index ()

    :   fMap ()

    {

    }

/*****************************************************************************/

bool dng_big_table_index::HasEntry (const dng_fingerprint &fingerprint) const
    {

    return fMap.find (fingerprint) != fMap.end ();

    }

/*****************************************************************************/

bool dng_big_table_index::GetEntry (const dng_fingerprint &fingerprint,
                                    uint32 &tableSize,
                                    uint64 &tableOffset) const
    {

    auto it = fMap.find (fingerprint);

    if (it != fMap.end ())
        {

        tableSize   = it->second.fTableSize;
        tableOffset = it->second.fTableOffset;

        return true;

        }

    return false;

    }

/*****************************************************************************/

void dng_big_table_index::AddEntry (const dng_fingerprint &fingerprint,
                                    uint32 tableSize,
                                    uint64 tableOffset)
    {

    if (fMap.find (fingerprint) == fMap.end ())
        {

        struct IndexEntry entry;

        entry.fTableSize   = tableSize;
        entry.fTableOffset = tableOffset;

        fMap.insert (std::pair<dng_fingerprint,
                               IndexEntry> (fingerprint, entry));


        }

    }

/*****************************************************************************/
/*****************************************************************************/
/*****************************************************************************/

bool dng_big_table_group_index::GetEntry (const dng_fingerprint &groupDigest,
                                          dng_fingerprint &instanceDigest) const
    {

    auto it = fMap.find (groupDigest);

    if (it != fMap.end ())
        {

        instanceDigest = it->second;

        return true;

        }

    return false;

    }

/*****************************************************************************/
/*****************************************************************************/
/*****************************************************************************/

dng_big_table_storage::dng_big_table_storage ()
    {

    }

/*****************************************************************************/

dng_big_table_storage::~dng_big_table_storage ()
    {

    }

/*****************************************************************************/

bool dng_big_table_storage::ReadTable (dng_big_table & /* table */,
                                       const dng_fingerprint & /* fingerprint */,
                                       dng_memory_allocator & /* allocator */)
    {

    return false;

    }

/*****************************************************************************/

bool dng_big_table_storage::WriteTable (const dng_big_table &table,
                                        const dng_fingerprint & /* fingerprint */,
                                        dng_memory_allocator & /* allocator */)
    {

    if (table.IsEmbedNever ())
        {
        return true;
        }

    return false;

    }

/*****************************************************************************/

void dng_big_table_storage::MissingTable (const dng_fingerprint & /* fingerprint */)
    {

    }

/*****************************************************************************/

bool dng_big_table_storage::GroupToInstance (const dng_fingerprint & /* groupDigest */,
                                             dng_fingerprint & /* instanceDigest */) const
    {

    return false;

    }

/*****************************************************************************/

dng_look_table::dng_look_table ()

    :   dng_big_table (&gLookTableCache)

    ,   fData   ()
    ,   fAmount (1.0)

    {

    }

/*****************************************************************************/

dng_look_table::dng_look_table (const dng_look_table &table)

    :   dng_big_table (table)

    ,   fData   (table.fData)
    ,   fAmount (table.fAmount)

    {

    }

/*****************************************************************************/

dng_look_table & dng_look_table::operator= (const dng_look_table &table)
    {

    dng_big_table::operator= (table);

    fData   = table.fData;
    fAmount = table.fAmount;

    return *this;

    }

/*****************************************************************************/

dng_look_table::~dng_look_table ()
    {

    }

/*****************************************************************************/

void dng_look_table::Set (const dng_hue_sat_map &map,
                          uint32 encoding)
    {

    fData.fMap      = map;
    fData.fEncoding = encoding;

    fData.ComputeMonochrome ();

    RecomputeFingerprint ();

    }

/*****************************************************************************/

bool dng_look_table::IsValid () const
    {

    if (IsMissing ())
        {
        return false;
        }

    return fData.fMap.IsValid ();

    }

/*****************************************************************************/

void dng_look_table::SetInvalid ()
    {

    *this = dng_look_table ();

    }

/*****************************************************************************/

bool dng_look_table::GetStream (dng_stream &stream)
    {

    table_data data;

    if (stream.Get_uint32 () != btt_LookTable)
        {
        return false;
        }

    uint32 version = stream.Get_uint32 ();

    if (version != kLookTableVersion1 &&
        version != kLookTableVersion2)
        {
        ThrowBadFormat ("Unknown look table version");
        }

    uint32 hueDivisions = stream.Get_uint32 ();
    uint32 satDivisions = stream.Get_uint32 ();
    uint32 valDivisions = stream.Get_uint32 ();

    if (hueDivisions < 1 || hueDivisions > kMaxHueSamples ||
        satDivisions < 1 || satDivisions > kMaxSatSamples ||
        valDivisions < 1 || valDivisions > kMaxValSamples ||
        (dng_safe_uint32 (hueDivisions) *
         dng_safe_uint32 (satDivisions) *
         dng_safe_uint32 (valDivisions)).Get () > kMaxTotalSamples)
        {
        ThrowBadFormat ();
        }

    data.fMap.SetDivisions (hueDivisions,
                            satDivisions,
                            valDivisions);

    uint32 count = data.fMap.DeltasCount ();

    dng_hue_sat_map::HSBModify * deltas = data.fMap.GetDeltas ();

    for (uint32 index = 0; index < count; index++)
        {

        deltas->fHueShift = stream.Get_real32 ();
        deltas->fSatScale = stream.Get_real32 ();
        deltas->fValScale = stream.Get_real32 ();

        deltas++;

        }

    data.fMap.AssignNewUniqueRuntimeFingerprint ();

    data.fEncoding = stream.Get_uint32 ();

    if (data.fEncoding != encoding_Linear &&
        data.fEncoding != encoding_sRGB)
        {
        ThrowBadFormat ("Unknown look table encoding");
        }

    if (version != kLookTableVersion1)
        {

        data.fMinAmount = stream.Get_real64 ();
        data.fMaxAmount = stream.Get_real64 ();

        if (data.fMinAmount < 0.0 || data.fMinAmount > 1.0 || data.fMaxAmount < 1.0)
            {
            ThrowBadFormat ("Invalid min/max amount for look table");
            }

        }

    else
        {

        data.fMinAmount = 1.0;
        data.fMaxAmount = 1.0;

        }

    data.ComputeMonochrome ();

    if (stream.Position () + 4 <= stream.Length ())
        {
        data.fFlags = stream.Get_uint32 ();
        }

    fData = data;

    return true;

    }

/*****************************************************************************/

void dng_look_table::PutStream (dng_stream &stream,
                                bool /* forFingerprint */) const
    {

    DNG_REQUIRE (IsValid (), "Invalid Look Table");

    stream.Put_uint32 (btt_LookTable);

    uint32 version = kLookTableVersion1;

    if (fData.fMinAmount != 1.0 ||
        fData.fMaxAmount != 1.0)
        {
        version = kLookTableVersion2;
        }

    stream.Put_uint32 (version);

    uint32 hueDivisions;
    uint32 satDivisions;
    uint32 valDivisions;

    fData.fMap.GetDivisions (hueDivisions,
                             satDivisions,
                             valDivisions);

    stream.Put_uint32 (hueDivisions);
    stream.Put_uint32 (satDivisions);
    stream.Put_uint32 (valDivisions);

    uint32 count = fData.fMap.DeltasCount ();

    const dng_hue_sat_map::HSBModify * deltas = fData.fMap.GetConstDeltas ();

    for (uint32 index = 0; index < count; index++)
        {

        stream.Put_real32 (deltas->fHueShift);
        stream.Put_real32 (deltas->fSatScale);
        stream.Put_real32 (deltas->fValScale);

        deltas++;

        }

    stream.Put_uint32 (fData.fEncoding);

    if (version != kLookTableVersion1)
        {

        stream.Put_real64 (fData.fMinAmount);
        stream.Put_real64 (fData.fMaxAmount);

        }

    if (fData.fFlags != 0)
        {

        stream.Put_uint32 (fData.fFlags);

        }

    }

/*****************************************************************************/

dng_rgb_table::dng_rgb_table ()

    :   dng_big_table (&gRGBTableCache)

    ,   fData   ()
    ,   fAmount (1.0)

    {

    }

/*****************************************************************************/

dng_rgb_table::dng_rgb_table (const dng_rgb_table &table)

    :   dng_big_table (table)

    ,   fData   (table.fData)
    ,   fAmount (table.fAmount)

    {

    }

/*****************************************************************************/

dng_rgb_table & dng_rgb_table::operator= (const dng_rgb_table &table)
    {

    dng_big_table::operator= (table);

    fData   = table.fData;
    fAmount = table.fAmount;

    return *this;

    }

/*****************************************************************************/

dng_rgb_table::~dng_rgb_table ()
    {

    }

/*****************************************************************************/

bool dng_rgb_table::IsValid () const
    {

    if (IsMissing ())
        {
        return false;
        }

    // If table itself is invalid, then invalid.

    if (fData.fDimensions == 0)
        return false;

    // If table has some effect, then valid.

    if (fAmount > 0.0)
        return true;

    // Does the matrix itself do any clipping?

    if (fData.fPrimaries == primaries_ProPhoto ||
        fData.fGamut     == gamut_extend       )
        return false;

    // Table is a NOP but there is some gamut clipping.

    return true;

    }

/*****************************************************************************/

void dng_rgb_table::SetInvalid ()
    {

    *this = dng_rgb_table ();

    }

/*****************************************************************************/

void dng_rgb_table::Set (uint32 dimensions,
                         uint32 divisions,
                         dng_ref_counted_block samples)
    {

    if (dimensions == 1)
        {

        if (divisions < kMinDivisions1D ||
            divisions > kMaxDivisions1D)
            {

            ThrowProgramError ("Bad 1D divisions");

            }

        if (samples.LogicalSize () != divisions * 4 * sizeof (uint16))
            {

            ThrowProgramError ("Bad 1D sample count");

            }

        }

    else if (dimensions == 3)
        {

        if (divisions < kMinDivisions3D ||
            divisions > kMaxDivisions3D_InMemory)
            {

            ThrowProgramError ("Bad 3D divisions");

            }

        if (samples.LogicalSize () != divisions *
                                      divisions *
                                      divisions * 4 * sizeof (uint16))
            {

            ThrowProgramError ("Bad 3D sample count");

            }

        }

    else
        {

        ThrowProgramError ("Bad dimensions");

        }

    fData.fDimensions = dimensions;

    fData.fDivisions = divisions;

    fData.fSamples = samples;

    fData.ComputeMonochrome ();

    RecomputeFingerprint ();

    }

/*****************************************************************************/

bool dng_rgb_table::GetStream (dng_stream &stream)
    {

    table_data data;

    if (stream.Get_uint32 () != btt_RGBTable)
        {
        return false;
        }

    if (stream.Get_uint32 () != kRGBTableVersion)
        {
        ThrowBadFormat ("Unknown RGB table version");
        }

    data.fDimensions = stream.Get_uint32 ();

    data.fDivisions = stream.Get_uint32 ();

    if (data.fDimensions == 1)
        {

        if (data.fDivisions < kMinDivisions1D ||
            data.fDivisions > kMaxDivisions1D)
            {
            ThrowBadFormat ("Invalid 1D divisions");
            }

        }

    else if (data.fDimensions == 3)
        {

        if (data.fDivisions < kMinDivisions3D ||
            data.fDivisions > kMaxDivisions3D)
            {
            ThrowBadFormat ("Invalid 3D divisions");
            }

        }

    else
        {
        ThrowBadFormat ("Invalid dimensions");
        }

    uint16 nopValue [kMaxDivisions1D > kMaxDivisions3D ? kMaxDivisions1D
                                                       : kMaxDivisions3D];

    for (uint32 index = 0; index < data.fDivisions; index++)
        {

        nopValue [index] = (uint16)
                           ((index * 0x0FFFF + (data.fDivisions >> 1)) /
                            (data.fDivisions - 1));

        }

    if (data.fDimensions == 1)
        {

        data.fSamples.Allocate (data.fDivisions * 4 * sizeof (uint16));

        uint16 *samples = data.fSamples.Buffer_uint16 ();

        for (uint32 index = 0; index < data.fDivisions; index++)
            {

            samples [0] = stream.Get_uint16 () + nopValue [index];
            samples [1] = stream.Get_uint16 () + nopValue [index];
            samples [2] = stream.Get_uint16 () + nopValue [index];
            samples [3] = 0;

            samples += 4;

            }

        }

    else
        {

        data.fSamples.Allocate (data.fDivisions *
                                data.fDivisions *
                                data.fDivisions * 4 * sizeof (uint16));

        uint16 *samples = data.fSamples.Buffer_uint16 ();

        for (uint32 rIndex = 0; rIndex < data.fDivisions; rIndex++)
            {

            for (uint32 gIndex = 0; gIndex < data.fDivisions; gIndex++)
                {

                for (uint32 bIndex = 0; bIndex < data.fDivisions; bIndex++)
                    {

                    samples [0] = stream.Get_uint16 () + nopValue [rIndex];
                    samples [1] = stream.Get_uint16 () + nopValue [gIndex];
                    samples [2] = stream.Get_uint16 () + nopValue [bIndex];
                    samples [3] = 0;

                    samples += 4;

                    }

                }

            }

        }

    uint32 primaries = stream.Get_uint32 ();

    if (primaries >= primaries_count)
        {
        ThrowBadFormat ("Unknown RGB table primaries");
        }

    data.fPrimaries = (primaries_enum) primaries;

    uint32 gamma = stream.Get_uint32 ();

    if (gamma >= gamma_count)
        {
        ThrowBadFormat ("Unknown RGB table gamma");
        }

    data.fGamma = (gamma_enum) gamma;

    uint32 gamut = stream.Get_uint32 ();

    if (gamut >= gamut_count)
        {
        ThrowBadFormat ("Unknown RGB table gamut processing option");
        }

    data.fGamut = (gamut_enum) gamut;

    data.fMinAmount = stream.Get_real64 ();
    data.fMaxAmount = stream.Get_real64 ();

    if (data.fMinAmount < 0.0 || data.fMinAmount > 1.0 || data.fMaxAmount < 1.0)
        {
        ThrowBadFormat ("Invalid min/max amount for RGB table");
        }

    data.ComputeMonochrome ();

    if (stream.Position () + 4 <= stream.Length ())
        {
        data.fFlags = stream.Get_uint32 ();
        }

    fData = data;

    return true;

    }

/*****************************************************************************/

void dng_rgb_table::PutStream (dng_stream &stream,
                               bool /* forFingerprint */) const
    {

    DNG_REQUIRE (IsValid (), "Invalid RGB Table");

    stream.Put_uint32 (btt_RGBTable);

    stream.Put_uint32 (kRGBTableVersion);

    stream.Put_uint32 (fData.fDimensions);

    stream.Put_uint32 (fData.fDivisions);

    uint16 nopValue [kMaxDivisions1D > kMaxDivisions3D_InMemory ? kMaxDivisions1D
                                                                : kMaxDivisions3D_InMemory];

    for (uint32 index = 0; index < fData.fDivisions; index++)
        {

        nopValue [index] = (uint16)
                           ((index * 0x0FFFF + (fData.fDivisions >> 1)) /
                            (fData.fDivisions - 1));

        }

    const uint16 *samples = fData.fSamples.Buffer_uint16 ();

    if (fData.fDimensions == 1)
        {

        for (uint32 index = 0; index < fData.fDivisions; index++)
            {

            stream.Put_uint16 (samples [0] - nopValue [index]);
            stream.Put_uint16 (samples [1] - nopValue [index]);
            stream.Put_uint16 (samples [2] - nopValue [index]);

            samples += 4;

            }

        }

    else
        {

        for (uint32 rIndex = 0; rIndex < fData.fDivisions; rIndex++)
            {

            for (uint32 gIndex = 0; gIndex < fData.fDivisions; gIndex++)
                {

                for (uint32 bIndex = 0; bIndex < fData.fDivisions; bIndex++)
                    {

                    stream.Put_uint16 (samples [0] - nopValue [rIndex]);
                    stream.Put_uint16 (samples [1] - nopValue [gIndex]);
                    stream.Put_uint16 (samples [2] - nopValue [bIndex]);

                    samples += 4;

                    }

                }

            }

        }

    stream.Put_uint32 ((uint32) fData.fPrimaries);

    stream.Put_uint32 ((uint32) fData.fGamma);

    stream.Put_uint32 ((uint32) fData.fGamut);

    stream.Put_real64 (fData.fMinAmount);
    stream.Put_real64 (fData.fMaxAmount);

    if (fData.fFlags != 0)
        {

        stream.Put_uint32 (fData.fFlags);

        }

    }

/*****************************************************************************/
/*****************************************************************************/
/*****************************************************************************/

dng_image_table_compression_info::~dng_image_table_compression_info ()
    {

    }

/*****************************************************************************/

void dng_image_table_compression_info::Compress (dng_host &host,
                                                 dng_stream &stream,
                                                 const dng_image &image) const
    {

    dng_image_writer writer;

    writer.WriteTIFFWithProfile (host,
                                 stream,
                                 image,
                                 image.Planes () >= 3
                                     ? piRGB
                                     : piBlackIsZero,
                                 image.PixelType () == ttShort
                                     ? ccJPEG       // Lossless JPEG
                                     : ccDeflate);

    }

/*****************************************************************************/
/*****************************************************************************/
/*****************************************************************************/

dng_image_table_jxl_compression_info::dng_image_table_jxl_compression_info ()

    :   fEncodeSettings (new dng_jxl_encode_settings)

    {

    }

/*****************************************************************************/

void dng_image_table_jxl_compression_info::Compress (dng_host &host,
                                                     dng_stream &stream,
                                                     const dng_image &image) const
    {

    DNG_REQUIRE (fEncodeSettings.Get (),
                 "Missing encode settings");

    #if 1

    // Use TIFF container but with JXL compression. This is better for larger
    // images since we can encode tiles in parallel.

    host.SetJXLEncodeSettings (*fEncodeSettings);

    host.SetJXLColorSpaceInfo (fColorSpaceInfo);

    dng_image_writer writer;

    writer.WriteTIFFWithProfile (host,
                                 stream,
                                 image,
                                 image.Planes () >= 3
                                     ? piRGB
                                     : piBlackIsZero,
                                 ccJXL,
                                 nullptr,    // metadata
                                 nullptr,    // profile data
                                 0,          // profile size,
                                 nullptr,    // resolution
                                 nullptr,    // thumbnail
                                 nullptr,    // image resources
                                 kMetadataSubset_All,
                                 false,      // has transparency
                                 true,       // allow big tiff
                                 nullptr,    // gain map,
                                 fPreferHalfFloat);

    #else

    // Use JXL directly.

    dng_jxl_color_space_info colorSpaceInfo;

    PreviewColorSpaceToJXLEncoding (previewColorSpace_MaxEnum,
                                    image.Planes (),
                                    colorSpaceInfo);

    EncodeJXL_Tile (host,
                    stream,
                    image,
                    colorSpaceInfo,
                    *fEncodeSettings);

    #endif

    }

/*****************************************************************************/
/*****************************************************************************/
/*****************************************************************************/

dng_image_table::dng_image_table ()

    :   dng_big_table (&gImageTableCache)

    {

    }

/*****************************************************************************/

dng_image_table::dng_image_table (const dng_image_table &table)

    :   dng_big_table (table)

    ,   fImage (table.fImage)

    ,   fCompressedData (table.fCompressedData)

    ,   fCompressionType (table.fCompressionType)

    {

    }

/*****************************************************************************/

dng_image_table & dng_image_table::operator= (const dng_image_table &table)
    {

    dng_big_table::operator= (table);

    fImage = table.fImage;

    fCompressedData = table.fCompressedData;

    fCompressionType = table.fCompressionType;

    return *this;

    }

/*****************************************************************************/

dng_image_table::~dng_image_table ()
    {

    }

/*****************************************************************************/

bool dng_image_table::IsValid () const
    {

    if (IsMissing ())
        {
        return false;
        }

    if (!fImage.get ())
        {
        return false;
        }

    return true;

    }

/*****************************************************************************/

void dng_image_table::SetInvalid ()
    {

    *this = dng_image_table ();

    RecomputeFingerprint ();

    }

/*****************************************************************************/

void dng_image_table::SetImage (const dng_image *image,
                                const dng_image_table_compression_info *compressionInfo,
                                dng_abort_sniffer *sniffer)
    {

    fImage = std::shared_ptr<const dng_image> (image);

    fCompressedData.reset ();

    if (compressionInfo && (compressionInfo->Type () > 0))
        CompressImage (*compressionInfo,
                       sniffer);

    RecomputeFingerprint ();

    }

/*****************************************************************************/

void dng_image_table::SetImage (const std::shared_ptr<const dng_image> &image,
                                const dng_image_table_compression_info *compressionInfo,
                                dng_abort_sniffer *sniffer)
    {

    if (fImage != image)
        {

        fImage = image;

        fCompressedData.reset ();

        if (compressionInfo && (compressionInfo->Type () > 0))
            CompressImage (*compressionInfo,
                           sniffer);

        RecomputeFingerprint ();

        }

    }

/*****************************************************************************/

dng_host * dng_image_table::MakeHost (dng_abort_sniffer *sniffer) const
    {

    return new dng_host (nullptr, sniffer);

    }

/*****************************************************************************/

dng_fingerprint dng_image_table::ComputeFingerprint () const
    {

    // If we're using lossy compression, then fingerprint the (compressed)
    // stream itself, including the header.

    if (fCompressedData)
        {

        AutoPtr<dng_host> host (MakeHost (nullptr));

        dng_memory_stream tempStream (host->Allocator ());

        PutStream (tempStream, true);

        tempStream.Flush ();

        tempStream.SetReadPosition (0);

        dng_md5_printer_stream stream;

        stream.SetLittleEndian ();

        tempStream.CopyToStream (stream,
                                 tempStream.Length ());

        auto digest = stream.Result ();

        return digest;

        }

    // Otherwise fingerprint the image itself (uncompressed case).

    if (fImage.get ())
        {

        AutoPtr<dng_host> host (MakeHost (nullptr));

        dng_md5_printer_stream stream;

        stream.SetLittleEndian ();

        stream.Put_uint32 (btt_ImageTable);

        stream.Put_uint32 (kImageTableVersion);

        stream.Put_int32 (fImage->Bounds ().t);
        stream.Put_int32 (fImage->Bounds ().l);
        stream.Put_int32 (fImage->Bounds ().b);
        stream.Put_int32 (fImage->Bounds ().r);

        stream.Put_uint32 (fImage->Planes ());

        stream.Put_uint32 (fImage->PixelType ());

        dng_fingerprint imageDigest = dng_negative::FindFastImageDigest
                                                    (*host,
                                                     *fImage,
                                                     fImage->PixelType ());

        stream.Put (imageDigest.data, 16);

        return stream.Result ();

        }

    return dng_fingerprint ();

    }

/*****************************************************************************/

static void CheckImageTableIFD (const dng_ifd &ifd)
    {

    dng_rect bounds = ifd.Bounds ();

    if (bounds.ShortSide () < 1 ||
        bounds.LongSide  () > kMaxImageSide)
        {
        ThrowBadFormat ();
        }

    uint32 planes = ifd.fSamplesPerPixel;

    if (planes < 1 || planes > kMaxColorPlanes)
        {
        ThrowBadFormat ();
        }

    uint32 pixelType = ifd.PixelType ();

    if (pixelType != ttByte  &&
        pixelType != ttShort &&
        pixelType != ttFloat)
        {
        ThrowBadFormat ();
        }

    }

/*****************************************************************************/

bool dng_image_table::GetStream (dng_stream &stream)
    {

    AutoPtr<dng_host> host (MakeHost (stream.Sniffer ()));

    if (stream.Get_uint32 () != btt_ImageTable)
        {
        return false;
        }

    if (stream.Get_uint32 () != kImageTableVersion)
        {
        ThrowBadFormat ("Unknown image table version");
        }

    dng_point imageTL;

    imageTL.v = stream.Get_int32 ();
    imageTL.h = stream.Get_int32 ();

    if (!stream.Data ())
        {
        ThrowProgramError ("Not a memory stream");
        }

    dng_stream subStream ((uint8 *) stream.Data () + stream.Position (),
                          (uint32) (stream.Length () - stream.Position ()));

    subStream.SetSniffer (stream.Sniffer ());

    AutoPtr<dng_image> image;

    dng_info jxlInfo;

    if (ParseJXL (*host,
                  subStream,
                  jxlInfo,
                  true,                      // bare codestream
                  false))                    // container
        {

        // Read as JXL.

        if (jxlInfo.IFDCount () < 1)
            {
            ThrowBadFormat ();
            }

        CheckImageTableIFD (*jxlInfo.fIFD [0]);

        subStream.SetReadPosition (0);

        dng_jxl_decoder decoder;

        decoder.fNeedBoxMeta = false;

        decoder.Decode (*host, subStream);

        image.Reset (decoder.fMainImage.Release ());

        fCompressionType = ccJXL;

        }

    else
        {

        // Read as TIFF.

        dng_info info;

        info.Parse (*host, subStream);

        info.PostParse (*host);

        if (info.fMagic != 42)
            {
            ThrowBadFormat ();
            }

        if (info.IFDCount () < 1)
            {
            ThrowBadFormat ();
            }

        const dng_ifd &ifd = *info.fIFD [0];

        CheckImageTableIFD (ifd);

        image.Reset (host->Make_dng_image (ifd.Bounds (),
                                           ifd.fSamplesPerPixel,
                                           ifd.PixelType ()));

        ifd.ReadImage (*host,
                       subStream,
                       *image);

        fCompressionType = ifd.fCompression;

        } // JXL vs TIFF

    // Grab a copy of the (lossy) compressed data.

    if (fCompressionType == ccJXL)
        {

        subStream.SetReadPosition (0);

        fCompressedData.reset (subStream.AsMemoryBlock (host->Allocator ()));

        }

    if (imageTL != dng_point (0, 0))
        {

        AutoPtr<dng_image> tempImage (image->Clone ());

        tempImage->Offset (imageTL);

        image.Reset (tempImage.Release ());

        }

    fImage = std::shared_ptr<const dng_image> (image.Release ());

    return true;

    }

/*****************************************************************************/

void dng_image_table::PutStream (dng_stream &stream,
                                 bool forFingerprint) const
    {

    dng_image_table_compression_info defaultInfo;

    PutCompressedStream (stream,
                         forFingerprint,
                         defaultInfo);

    }

/*****************************************************************************/

void dng_image_table::PutCompressedStream (dng_stream &stream,
                                           bool /* forFingerprint */,
                                           const dng_image_table_compression_info &info) const
    {

    AutoPtr<dng_host> host (MakeHost (stream.Sniffer ()));

    stream.Put_uint32 (btt_ImageTable);

    stream.Put_uint32 (kImageTableVersion);

    stream.Put_int32 (fImage->Bounds ().t);
    stream.Put_int32 (fImage->Bounds ().l);

    const dng_image *tiffImage = fImage.get ();

    AutoPtr<dng_image> tempImage;

    if (tiffImage->Bounds ().TL () != dng_point (0, 0))
        {

        tempImage.Reset (tiffImage->Clone ());

        tempImage->Offset (dng_point (0, 0) - fImage->Bounds ().TL ());

        tiffImage = tempImage.Get ();

        }

    // If we have compressed data, then just write that directly.

    if (fCompressedData)
        {

        // printf ("--- writing compressed\n");

        stream.Put (fCompressedData->Buffer      (),
                    fCompressedData->LogicalSize ());

        }

    // Otherwise use the provided compression info.

    else
        {

        dng_memory_stream tempStream (host->Allocator ());

        info.Compress (*host,
                       tempStream,
                       *tiffImage);

        // Remember the compressed data.

        if (info.Type () != 0)
            {

            tempStream.SetReadPosition (0);

            fCompressedData.reset (tempStream.AsMemoryBlock (host->Allocator ()));

            }

        tempStream.SetReadPosition (0);

        tempStream.CopyToStream (stream, tempStream.Length ());

        }

    }

/*****************************************************************************/

void dng_image_table::CompressImage (const dng_image_table_compression_info &info,
                                     dng_abort_sniffer *sniffer)
    {

    fCompressionType = info.Type ();

    if (!fImage           ||
        info.Type () == 0 ||
        info.Type () == ccUncompressed)
        {
        return;
        }

    // Force the image go thru a write-read (encode-decode) cycle so that the
    // image stored in this object reflects errors introduced by the lossy
    // codec.

    AutoPtr<dng_host> host (MakeHost (sniffer));

    dng_memory_stream tempStream (host->Allocator ());

    tempStream.SetSniffer (sniffer);

    PutCompressedStream (tempStream,
                         false,
                         info);

    // Cannot just reset tempStream read position to 0 and call GetStream on
    // it directly because dng_image_table::GetStream implementation currently
    // relies on the stream having the data in one contiguous chunk. So make a
    // copy of the data and then feed the result to GetStream.

    AutoPtr<dng_memory_block> block (tempStream.AsMemoryBlock (host->Allocator ()));

    dng_stream readStream (block->Buffer (),
                           block->LogicalSize ());

    readStream.SetSniffer (sniffer);

    GetStream (readStream);

    fCompressionType = info.Type ();

    }

/*****************************************************************************/

void dng_image_table::SetData (const dng_image_table_data &data)
    {

    fImage           = data.fImage;

    fCompressedData  = data.fCompressedData;

    fCompressionType = data.fCompressionType;

    }

/*****************************************************************************/

void dng_image_table::GetData (dng_image_table_data &data) const
    {

    data.fImage           = fImage;

    data.fCompressedData  = fCompressedData;

    data.fCompressionType = fCompressionType;

    }

/*****************************************************************************/
/*****************************************************************************/
/*****************************************************************************/

dng_packed_image_table::dng_packed_image_table ()

    :   dng_big_table (&gPackedImageTableCache)

    {

    }

/*****************************************************************************/

dng_packed_image_table::dng_packed_image_table (const dng_packed_image_table &table)

    :   dng_big_table (table)

    ,   fTableDigest  (table.fTableDigest)
    ,   fTable        (table.CloneTable ())
    ,   fBlock        (table.fBlock)

    ,   fSize         (table.fSize)
    ,   fPlanes       (table.fPlanes)
    ,   fPixelType    (table.fPixelType)

    {

    }

/*****************************************************************************/

dng_packed_image_table & dng_packed_image_table::operator= (const dng_packed_image_table &table)
    {

    dng_big_table::operator= (table);

    fTableDigest = table.fTableDigest;
    fBlock       = table.fBlock;

    fTable.reset (table.CloneTable ());

    fSize      = table.fSize;
    fPlanes    = table.fPlanes;
    fPixelType = table.fPixelType;

    return *this;

    }

/*****************************************************************************/

const dng_image_table & dng_packed_image_table::Table () const
    {

    DNG_REQUIRE (fTable, "Invalid table");

    return *fTable;

    }

/*****************************************************************************/

bool dng_packed_image_table::IsValid () const
    {

    return (fTableDigest.IsValid () &&
            ((fTable && fTable->IsValid ()) || (fBlock != nullptr)));

    }

/*****************************************************************************/

uint32 dng_packed_image_table::PackedBytes () const
    {

    return fBlock ? fBlock->LogicalSize () : 0;

    }

/*****************************************************************************/

dng_host * dng_packed_image_table::MakeHost (dng_abort_sniffer *sniffer) const
    {

    return new dng_host (nullptr, sniffer);

    }

/*****************************************************************************/

dng_image_table * dng_packed_image_table::MakeTable () const
    {

    return new dng_image_table;

    }

/*****************************************************************************/

dng_image_table * dng_packed_image_table::CloneTable () const
    {

    return fTable ? (new dng_image_table (*fTable)) : nullptr;

    }

/*****************************************************************************/

void dng_packed_image_table::Clear ()
    {

    fTableDigest.Clear ();

    fTable.reset ();

    fBlock.reset ();

    fSize = dng_point ();

    fPlanes = 0;

    fPixelType = 0;

    }

/*****************************************************************************/

void dng_packed_image_table::ClearPackedData ()
    {

    fBlock.reset ();

    }

/*****************************************************************************/

void dng_packed_image_table::SetImage (const dng_image *image,
                                       const dng_image_table_compression_info *compressionInfo,
                                       dng_abort_sniffer *sniffer)
    {

    if (!image)
        {
        Clear ();
        return;
        }

    AutoPtr<dng_image_table> table (MakeTable ());

    fSize      = image->Size ();
    fPlanes    = image->Planes ();
    fPixelType = image->PixelType ();

    table->SetImage (image, compressionInfo, sniffer);

    fTable.reset (table.Release ());

    fTableDigest = fTable->Fingerprint ();

    ClearPackedData ();

    RecomputeFingerprint ();

    }

/*****************************************************************************/

void dng_packed_image_table::SetImage (const std::shared_ptr<const dng_image> &image,
                                       const dng_image_table_compression_info *compressionInfo,
                                       dng_abort_sniffer *sniffer)
    {

    if (!image)
        {
        Clear ();
        return;
        }

    if (fTable && (fTable->ShareImage () == image))
        return;

    fSize      = image->Size ();
    fPlanes    = image->Planes ();
    fPixelType = image->PixelType ();

    AutoPtr<dng_image_table> table (MakeTable ());

    table->SetImage (image, compressionInfo, sniffer);

    fTable.reset (table.Release ());

    fTableDigest = fTable->Fingerprint ();

    ClearPackedData ();

    RecomputeFingerprint ();

    }

/*****************************************************************************/

void dng_packed_image_table::Unpack (dng_abort_sniffer *sniffer)
    {

    DNG_REQUIRE (fBlock,
                 "Cannot unpack invalid block");

    AutoPtr<dng_host> host (MakeHost (sniffer));

    AutoPtr<dng_image_table> table (MakeTable ());

    if (!table->DecodeFromBinary (*host,
                                  fBlock->Buffer_uint8 (),
                                  fBlock->LogicalSize ()))
        {

        ThrowBadFormat ("Could not Unpack block to cr_image_table");

        }

    fTable.reset (table.Release ());

    if (fTable->Fingerprint () != fTableDigest)
        {

        DNG_REPORT ("fTableDigest does not match table fingerprint");

        }

    #if 0
    fSize      = Image ().Size ();
    fPlanes    = Image ().Planes ();
    fPixelType = Image ().Size ();
    #endif

    ClearPackedData ();

    }

/*****************************************************************************/

void dng_packed_image_table::Pack (dng_abort_sniffer *sniffer)
    {

    DNG_REQUIRE (IsValidUnpacked (),
                 "Cannot pack invalid table");

    AutoPtr<dng_host> host (MakeHost (sniffer));

    uint32 compressedSize = 0;

    fBlock.reset (fTable->EncodeAsBinary (host->Allocator (),
                                          compressedSize));

    fTable.reset ();

    }

/*****************************************************************************/

bool dng_packed_image_table::GetStream (dng_stream &stream)
    {

    if (stream.Get_uint32 () != btt_PackedImageTable)
        return false;

    if (stream.Get_uint32 () != kPackedImageTableVersion)
        ThrowBadFormat ("Unknown packed image table version");

    // Clear the table.

    fTable.reset ();

    // Read digest.

    stream.Get (fTableDigest.data, 16);

    // Read size.

    fSize.h = int32 (stream.Get_uint32 ());
    fSize.v = int32 (stream.Get_uint32 ());

    if (fSize.h <= 0)
        ThrowBadFormat ("Invalid size.h in packed image table");

    if (fSize.v <= 0)
        ThrowBadFormat ("Invalid size.v in packed image table");

    // Read planes.

    fPlanes = stream.Get_uint32 ();

    if (fPlanes == 0 || fPlanes > kMaxColorPlanes)
        ThrowBadFormat ("Invalid planes in packed image table");

    // Read pixel type.

    fPixelType = stream.Get_uint32 ();

    if (fPixelType == 0)
        ThrowBadFormat ("Invalid pixel type in packed image table");

    // Read bytes of image table. Currently limit compressed image tables
    // to 32-bit.

    const uint32 bytes = stream.Get_uint32 ();

    AutoPtr<dng_host> host (MakeHost (stream.Sniffer ()));

    AutoPtr<dng_memory_block> block (host->Allocate (bytes));

    stream.Get (block->Buffer (), bytes);

    fBlock.reset (block.Release ());

    return true;

    }

/*****************************************************************************/

void dng_packed_image_table::PutStream (dng_stream &stream,
                                        bool /* forFingerprint */) const
    {

    DNG_REQUIRE (IsValid (),
                 "Called PutStream on invalid packed image table");

    // Write Big Table Type.

    stream.Put_uint32 (btt_PackedImageTable);

    // Write Version.

    stream.Put_uint32 (kPackedImageTableVersion);

    // Write digest.

    stream.Put (fTableDigest.data, 16);

    // Write properties.

    stream.Put_uint32 (uint32 (fSize.h));
    stream.Put_uint32 (uint32 (fSize.v));

    stream.Put_uint32 (fPlanes);

    stream.Put_uint32 (fPixelType);

    // If the block is valid, just use that.

    std::shared_ptr<const dng_memory_block> block;

    if (fBlock)
        block = fBlock;

    // Otherwise, first encode the table into a temporary block.

    else
        {

        DNG_REQUIRE (fTable, "missing fTable");

        AutoPtr<dng_host> host (MakeHost (stream.Sniffer ()));

        uint32 compressedSize = 0;

        block.reset
            (fTable->EncodeAsBinary (host->Allocator (),
                                     compressedSize));

        }

    DNG_REQUIRE (block,
                 "Missing block");

    // Write byte length.

    stream.Put_uint32 (block->LogicalSize ());

    // Write data.

    stream.Put (block->Buffer (),
                block->LogicalSize ());

    }

/*****************************************************************************/
/*****************************************************************************/
/*****************************************************************************/

dng_masked_rgb_table::dng_masked_rgb_table ()
    {

    }

/*****************************************************************************/

dng_masked_rgb_table::dng_masked_rgb_table (dng_string tableSemanticName,
                                            uint32 pixelType,
                                            const dng_rgb_table &table)

    :   fTableSemanticName (tableSemanticName)

    ,   fPixelType (pixelType)

    ,   fTable (table)

    {

    Validate ();

    }

/*****************************************************************************/

void dng_masked_rgb_table::Validate () const
    {

    // Name cannot be longer than 16-bit.

    if (fTableSemanticName.Length () > 65535)
        {
        ThrowBadFormat ("TableSemanticName too long in RGBTables");
        }

    // Check PixelType.

    CheckPixelType (fPixelType);

    // Check Divisions.

    CheckDivisions (fTable.Divisions ());

    // Check GammaEncoding.

    CheckGammaEncoding (fTable.Gamma ());

    // Check ColorPrimaries.

    CheckColorPrimaries (fTable.Primaries ());

    // Check GamutExtension.

    CheckGamutExtension (fTable.Gamut ());

    // Must be 3D table.

    DNG_REQUIRE (fTable.Dimensions () == 3,
                 "RGBTables must have dimension value of 3");

    // For now don't perform any validation on the table data itself.

    }

/*****************************************************************************/

static uint8 ColorPrimariesEnumToRGBTablesTagValue
    (dng_rgb_table::primaries_enum value)
    {

    switch (value)
        {

        case dng_rgb_table::primaries_sRGB:     return 0;
        case dng_rgb_table::primaries_Adobe:    return 1;
        case dng_rgb_table::primaries_P3:       return 2;
        case dng_rgb_table::primaries_Rec2020:  return 3;
        case dng_rgb_table::primaries_ProPhoto: return 4;

        default:
            ThrowProgramError ("unsupported primaries_enum value");
            break;

        }

    return 0;                                // sRGB

    }

/*****************************************************************************/

static dng_rgb_table::primaries_enum ColorPrimariesValueToEnum (uint32 x)
    {

    switch (x)
        {

        case 0: return dng_rgb_table::primaries_sRGB;
        case 1: return dng_rgb_table::primaries_Adobe;
        case 2: return dng_rgb_table::primaries_P3;
        case 3: return dng_rgb_table::primaries_Rec2020;
        case 4: return dng_rgb_table::primaries_ProPhoto;

        default:
            {
            ThrowProgramError ("unsupported ColorPrimaries value");
            break;
            }

        }

    return dng_rgb_table::primaries_sRGB;

    }

/*****************************************************************************/

static uint32 GamutExtensionEnumToRGBTablesTagValue (dng_rgb_table::gamut_enum value)
    {

    switch (value)
        {

        case dng_rgb_table::gamut_clip:   return 0;
        case dng_rgb_table::gamut_extend: return 1;

        default:
            ThrowProgramError ("unsupported gamut_enum value");
            break;

        }

    return 1;                                // extend

    }

/*****************************************************************************/

static dng_rgb_table::gamut_enum GamutValueToEnum (uint32 x)
    {

    switch (x)
        {

        case 0: return dng_rgb_table::gamut_clip;
        case 1: return dng_rgb_table::gamut_extend;

        default:
            {
            ThrowProgramError ("Unexpected GamutExtension value");
            break;
            }

        }

    return dng_rgb_table::gamut_extend;

    }

/*****************************************************************************/

static uint8 GammaEnumToRGBTablesTagValue (dng_rgb_table::gamma_enum value)
    {

    switch (value)
        {

        case dng_rgb_table::gamma_Linear:  return 0;
        case dng_rgb_table::gamma_sRGB:    return 1;
        case dng_rgb_table::gamma_1_8:     return 2;
        case dng_rgb_table::gamma_2_2:     return 3;
        case dng_rgb_table::gamma_Rec2020: return 4;

        default:
            ThrowProgramError ("unsupported gamma_enum value");
            break;

        }

    return 1;                                // sRGB

    }

/*****************************************************************************/

static dng_rgb_table::gamma_enum GammaValueToEnum (uint32 x)
    {

    switch (x)
        {

        case 0: return dng_rgb_table::gamma_Linear;
        case 1: return dng_rgb_table::gamma_sRGB;
        case 2: return dng_rgb_table::gamma_1_8;
        case 3: return dng_rgb_table::gamma_2_2;
        case 4: return dng_rgb_table::gamma_Rec2020;

        default:
            {
            ThrowProgramError ("Unexpected GammaEncoding value");
            break;
            }

        }

    return dng_rgb_table::gamma_sRGB;

    }

/*****************************************************************************/

void dng_masked_rgb_table::CheckDivisions (uint32 divisions)
    {

    if (divisions < 2 || divisions > 32)
        {
        ThrowBadFormat ("Invalid Divisions in RGBTables");
        }

    }

/*****************************************************************************/

void dng_masked_rgb_table::CheckPixelType (uint32 pixelType)
    {

    if (pixelType > 2)
        {
        ThrowBadFormat ("Invalid PixelType in RGBTables");
        }

    }

/*****************************************************************************/

void dng_masked_rgb_table::CheckGammaEncoding (dng_rgb_table::gamma_enum gamma)
    {

    switch (gamma)
        {

        // Enumerate the ones supported in the tag.

        case dng_rgb_table::gamma_Linear:
        case dng_rgb_table::gamma_sRGB:
        case dng_rgb_table::gamma_1_8:
        case dng_rgb_table::gamma_2_2:
        case dng_rgb_table::gamma_Rec2020:
            break;

        // Reject everything else.

        default:
            ThrowBadFormat ("Unsupported GammaEncoding in RGBTables");

        }

    }

/*****************************************************************************/

void dng_masked_rgb_table::CheckColorPrimaries (dng_rgb_table::primaries_enum primaries)
    {

    switch (primaries)
        {

        // Enumerate the ones supported in the tag.

        case dng_rgb_table::primaries_Adobe:
        case dng_rgb_table::primaries_P3:
        case dng_rgb_table::primaries_ProPhoto:
        case dng_rgb_table::primaries_Rec2020:
        case dng_rgb_table::primaries_sRGB:
            break;

        // Reject everything else.

        default:
            ThrowBadFormat ("Unsupported ColorPrimaries in RGBTables");

        }

    }

/*****************************************************************************/

void dng_masked_rgb_table::CheckGamutExtension (dng_rgb_table::gamut_enum gamut)
    {

    switch (gamut)
        {

        // Enumerate the ones supported in the tag.

        case dng_rgb_table::gamut_clip:
        case dng_rgb_table::gamut_extend:
            break;

        // Reject everything else.

        default:
            ThrowBadFormat ("Unsupported GamutExtension in RGBTables");

        }

    }

/*****************************************************************************/

void dng_masked_rgb_table::GetStream (dng_host &host,
                                      dng_stream &stream)
    {

    // Read LengthTableSemanticName and TableSemanticName.

    uint16 nameLen = stream.Get_uint16 ();

    dng_memory_data nameData (nameLen + 1);

    stream.Get (nameData.Buffer (), nameLen);

    nameData.Buffer_uint8 () [nameLen] = 0;  // add null-term char

    fTableSemanticName.Set (nameData.Buffer_char ());

    // Read Divisions.

    uint32 divisions = (uint32) stream.Get_uint8 ();

    CheckDivisions (divisions);

    // Read PixelType.

    fPixelType = (uint32) stream.Get_uint8 ();

    CheckPixelType (fPixelType);

    // Read GammaEncoding.

    uint32 gamma = (uint32) stream.Get_uint8 ();

    auto gammaEnum = GammaValueToEnum (gamma);

    CheckGammaEncoding (gammaEnum);

    // Read ColorPrimaries.

    uint32 primaries = (uint32) stream.Get_uint8 ();

    auto primariesEnum = ColorPrimariesValueToEnum (primaries);

    CheckColorPrimaries (primariesEnum);

    // Read GamutExtension.

    uint32 gamut = (uint32) stream.Get_uint8 ();

    auto gamutEnum = GamutValueToEnum (gamut);

    CheckGamutExtension (gamutEnum);

    // Read samples.

    uint32 srcPixelSize = 1;

         if (fPixelType == 1) srcPixelSize = 2;
    else if (fPixelType == 2) srcPixelSize = 4;

    uint32 srcBytes = divisions * divisions * divisions * 3 * srcPixelSize;

    uint32 dstBytes = divisions * divisions * divisions * 4 * 2;

    dng_ref_counted_block samples;

    samples.Allocate (dstBytes);

    uint16 *dst = samples.Buffer_uint16 ();

    const uint32 divs = divisions;

    // Read into a stored data block.

    fStoredData.reset (host.Allocate (srcBytes));

    stream.Get (fStoredData->Buffer (),
                srcBytes);

    if (fPixelType == 0)                     // u8
        {

        const uint8 *src = fStoredData->Buffer_uint8 ();

        constexpr uint16 scale8to16 = 257; // 65535 / 255

        for (uint32 rIndex = 0; rIndex < divs; rIndex++)
            {

            for (uint32 gIndex = 0; gIndex < divs; gIndex++)
                {

                for (uint32 bIndex = 0; bIndex < divs; bIndex++)
                    {

                    dst [0] = uint16 (src [0]) * scale8to16;
                    dst [1] = uint16 (src [1]) * scale8to16;
                    dst [2] = uint16 (src [2]) * scale8to16;
                    dst [3] = 0;

                    src += 3;
                    dst += 4;

                    } // b

                } // g

            } // r

        }

    else if (fPixelType == 1)                // u16
        {

        const uint16 *src = fStoredData->Buffer_uint16 ();

        for (uint32 rIndex = 0; rIndex < divs; rIndex++)
            {

            for (uint32 gIndex = 0; gIndex < divs; gIndex++)
                {

                for (uint32 bIndex = 0; bIndex < divs; bIndex++)
                    {

                    dst [0] = src [0];
                    dst [1] = src [1];
                    dst [2] = src [2];
                    dst [3] = 0;

                    src += 3;
                    dst += 4;

                    } // b

                } // g

            } // r

        }

    else if (fPixelType == 2)                // fp32
        {

        // Map 1.0f to 65535.

        const real32 *src = fStoredData->Buffer_real32 ();

        const real32 kScale = 65535.0f;

        for (uint32 rIndex = 0; rIndex < divs; rIndex++)
            {

            for (uint32 gIndex = 0; gIndex < divs; gIndex++)
                {

                for (uint32 bIndex = 0; bIndex < divs; bIndex++)
                    {

                    dst [0] = Round_int32 (kScale * src [0]);
                    dst [1] = Round_int32 (kScale * src [1]);
                    dst [2] = Round_int32 (kScale * src [2]);
                    dst [3] = 0;

                    src += 3;
                    dst += 4;

                    } // b

                } // g

            } // r

        }

    else
        {

        ThrowProgramError ("Unexpected fPixelType");

        }

    const uint32 kDimensions = 3;

    fTable.Set (kDimensions,
                divisions,
                samples);

    fTable.SetGamut (gamutEnum);

    fTable.SetGamma (gammaEnum);

    fTable.SetPrimaries (primariesEnum);

    }

/*****************************************************************************/

void dng_masked_rgb_table::PutStream (dng_stream &stream) const
    {

    // LengthTableSemanticName.

    uint32 len = fTableSemanticName.Length ();

    stream.Put_uint16 ((uint16) len);

    // TableSemanticName.

    stream.Put (fTableSemanticName.Get (), len);

    // Divisions.

    stream.Put_uint8 ((uint8) fTable.Divisions ());

    // PixelType.

    stream.Put_uint8 ((uint8) fPixelType);

    // GammaEncoding.

    stream.Put_uint8 (GammaEnumToRGBTablesTagValue (fTable.Gamma ()));

    // ColorPrimaries.

    stream.Put_uint8 (ColorPrimariesEnumToRGBTablesTagValue (fTable.Primaries ()));

    // GamutExtension.

    stream.Put_uint8 (GamutExtensionEnumToRGBTablesTagValue (fTable.Gamut ()));

    // Table sample data.

    // If we have stored data, just write that.

    if (fStoredData.get ())
        {

        stream.Put (fStoredData->Buffer      (),
                    fStoredData->LogicalSize ());

        }

    // Otherwise derive the data from the internal table representation.

    else
        {

        // The data is stored in R, G, B order (R = outermost, B = innermost). The
        // dng_rgb_table class already stores its samples in this order, so just
        // walk through the values in linear order.

        const uint16 *samples = fTable.Samples ();

        const uint32 divs = fTable.Divisions ();

        if (fPixelType == 1)
            {

            // Output format is 16-bit unsigned, which matches our internal
            // representation, so no conversion needed.

            for (uint32 rIndex = 0; rIndex < divs; rIndex++)
                {

                for (uint32 gIndex = 0; gIndex < divs; gIndex++)
                    {

                    for (uint32 bIndex = 0; bIndex < divs; bIndex++)
                        {

                        stream.Put_uint16 (samples [0]);
                        stream.Put_uint16 (samples [1]);
                        stream.Put_uint16 (samples [2]);

                        samples += 4;

                        } // b

                    } // g

                } // r

            }

        else if (fPixelType == 0)
            {

            // Output format is 8-bit unsigned, so we need to convert.

            for (uint32 rIndex = 0; rIndex < divs; rIndex++)
                {

                for (uint32 gIndex = 0; gIndex < divs; gIndex++)
                    {

                    for (uint32 bIndex = 0; bIndex < divs; bIndex++)
                        {

                        stream.Put_uint8 (uint8 ((int32 (samples [0]) + 128) / 257));
                        stream.Put_uint8 (uint8 ((int32 (samples [1]) + 128) / 257));
                        stream.Put_uint8 (uint8 ((int32 (samples [2]) + 128) / 257));

                        samples += 4;

                        } // b

                    } // g

                } // r

            }

        else if (fPixelType == 2)
            {

            // Output format is fp32, so we need to convert.

            const real32 kScale = 1.0f / 65535.0f;

            for (uint32 rIndex = 0; rIndex < divs; rIndex++)
                {

                for (uint32 gIndex = 0; gIndex < divs; gIndex++)
                    {

                    for (uint32 bIndex = 0; bIndex < divs; bIndex++)
                        {

                        stream.Put_real32 (kScale * samples [0]);
                        stream.Put_real32 (kScale * samples [1]);
                        stream.Put_real32 (kScale * samples [2]);

                        samples += 4;

                        } // b

                    } // g

                } // r

            }

        else
            {

            ThrowProgramError ("Unsupported PixelType in "
                               "dng_masked_rgb_table::PutStream");

            }

        }

    }

/*****************************************************************************/

void dng_masked_rgb_table::AddDigest (dng_md5_printer &printer) const
    {

    // Header.

    printer.Process ("dng_masked_rgb_table", 20);

    // Name.

    uint32 nameLen = SemanticName ().Length ();

    printer.Process (&nameLen,
                     (uint32) sizeof (nameLen));

    if (nameLen > 0)
        {

        printer.Process (SemanticName ().Get (), nameLen);

        }

    // Pixel type.

    printer.Process (&fPixelType,
                     (uint32) sizeof (fPixelType));

    // Table digest.

    dng_fingerprint tableDigest = fTable.Fingerprint ();

    printer.Process (tableDigest.data,
                     sizeof (tableDigest.data));

    }

/*****************************************************************************/
/*****************************************************************************/
/*****************************************************************************/

void dng_masked_rgb_table_render_data::Initialize (const dng_negative &negative,
                                                   const dng_camera_profile &profile)
    {

    if (!profile.HasMaskedRGBTables ())
        return;

    auto maskedTablesReference = profile.ShareMaskedRGBTables ();

    const auto &tables = *maskedTablesReference;

    if (tables.IsNOP ())
        return;

    fUseSequentialMethod = tables.UseSequentialMethod ();

    // Find correspondence between RGBTables and SemanticMasks tags. It is
    // still possible that we have a NOP situation if every table uses a mask
    // name that is not found in SemanticMasks.

    // First, make a hashtable of semantic masks, with the name as a key.

    std::unordered_map<dng_string,
                       dng_semantic_mask,
                       dng_string_hash> smMap;

        {

        const uint32 numMasks = negative.NumSemanticMasks ();

        for (uint32 i = 0; i < numMasks; i++)
            {

            const auto &mask = negative.SemanticMask (i);

            smMap.insert (std::make_pair (mask.fName, mask));

            }

        }

    // Next, walk through all tables in the RGBTables tag and figure out which
    // ones are relevant (have a matching SemanticMask label, or have no label
    // which means a background table).

    int32 debugIndex = 0;

    #if !qDebugMaskedRGBTableRender
    (void) debugIndex;
    #endif

    for (const auto &table : tables.Tables ())
        {

        DNG_REQUIRE (table, "bad table");

        const auto &name = table->SemanticName ();

        if (name.IsEmpty ())
            {

            DNG_REQUIRE (fBackgroundTable == nullptr,
                         "already have a background table");

            fBackgroundTable = table;

            if (fUseSequentialMethod)
                {

                dng_semantic_mask emptyMask;

                fMaskedTables.push_back
                    (std::make_pair (table, emptyMask));

                }

            #if qDebugMaskedRGBTableRender

            printf ("table %d will be treated as background table\n",
                    debugIndex);

            #endif

            }

        else
            {

            // Check if we have a corresponding semantic mask.

            auto iter = smMap.find (name);

            if (iter != smMap.end ())
                {

                // Found it. Add it to the list.

                fMaskedTables.push_back
                    (std::make_pair (table, iter->second));

                #if qDebugMaskedRGBTableRender

                printf ("table index %d -> semantic name '%s' found\n",
                        debugIndex,
                        name.Get ());

                #endif

                }

            #if qDebugMaskedRGBTableRender

            else
                {

                printf ("table index %d -> semantic name '%s' not found among "
                        "negative's list of semantic masks -- ignoring",
                        debugIndex,
                        name.Get ());

                }

            #endif

            }

        ++debugIndex;

        }

    // Find the background table index.

    fBackgroundTableIndex = uint32 (fMaskedTables.size ());

    if (fUseSequentialMethod)
        {

        for (size_t i = 0; i < fMaskedTables.size (); i++)
            {

            const auto &semanticMask = fMaskedTables [i].second;

            const_dng_image_sptr baseMask = semanticMask.fMask;

            // Empty base mask indicates a background table.

            if (!baseMask)
                {

                fBackgroundTableIndex = (uint32) i;

                break;

                }

            }

        DNG_REQUIRE ((!fBackgroundTable) ==
                     (fBackgroundTableIndex == fMaskedTables.size ()),
                     "inconsistent background table info for sequential");

        }

    }

/*****************************************************************************/

void dng_masked_rgb_table_render_data::PrepareRGBtoRGBTableData (dng_host &host)
    {

    fMaskedTableData.clear ();

    fMaskedTableData.reserve (fMaskedTables.size ());

    for (const auto &x : fMaskedTables)
        {

        dng_rgb_to_rgb_table_data_sptr ptr
            (host.Make_dng_rgb_to_rgb_table_data (x.first->Table ()));

        fMaskedTableData.push_back (ptr);

        }

    if (fBackgroundTable)
        {

        fBackgroundTableData.Reset
            (host.Make_dng_rgb_to_rgb_table_data (fBackgroundTable->Table ()));

        }

    }

/*****************************************************************************/
/*****************************************************************************/
/*****************************************************************************/

#if qDNGValidate

void dng_masked_rgb_table::Dump (uint32 indent) const
    {

    const uint32 kIndentBufLen = 32;

    indent = Min_uint32 (indent, kIndentBufLen - 1);

    char indentBuf [kIndentBufLen];

    snprintf (indentBuf,
              kIndentBufLen,
              "%*s",
              indent,
              "");

    printf ("%s%-20s: %s\n",
            indentBuf,
            "SemanticName",
            fTableSemanticName.Get ());

    printf ("%s%-20s: %u\n",
            indentBuf,
            "Divisions",
            fTable.Divisions ());

    const char *pixelTypeStrings [] = { "u8", "u16", "fp32" };

    printf ("%s%-20s: %u (%s)\n",
            indentBuf,
            "PixelType",
            fPixelType,
            pixelTypeStrings [Pin_uint32 (0, fPixelType, 2)]);

    const char *gammaEncodingStrings [] =
        {
        "linear",
        "sRGB",
        "1.8",
        "2.2",
        "Rec. 2020"
        };

    uint32 gammaTagValue = GammaEnumToRGBTablesTagValue (fTable.Gamma ());

    printf ("%s%-20s: %u (%s)\n",
            indentBuf,
            "GammaEncoding",
            gammaTagValue,
            gammaEncodingStrings [gammaTagValue]);

    const char *primariesStrings [] =
        {
        "sRGB",
        "Adobe RGB",
        "Display P3",
        "Rec. 2020",
        "ProPhoto"
        };

    uint32 primariesTagValue =
        ColorPrimariesEnumToRGBTablesTagValue (fTable.Primaries ());

    printf ("%s%-20s: %u (%s)\n",
            indentBuf,
            "ColorPrimaries",
            primariesTagValue,
            primariesStrings [primariesTagValue]);

    const char *gamutStrings [] =
        {
        "clip",
        "extend",
        };

    uint32 gamutTagValue =
        GamutExtensionEnumToRGBTablesTagValue (fTable.Gamut ());

    printf ("%s%-20s: %u (%s)\n",
            indentBuf,
            "GamutExtension",
            gamutTagValue,
            gamutStrings [gamutTagValue]);

    const uint32 divs = fTable.Divisions ();

    const uint16 *samples = fTable.Samples ();

    bool doPrint = true;

    uint32 linesPrinted = 0;

    const uint8 *storedPtr = nullptr;

    if (fStoredData.get ())
        {
        storedPtr = fStoredData->Buffer_uint8 ();
        }

    for (uint32 rIndex = 0; rIndex < divs && doPrint; rIndex++)
        {

        for (uint32 gIndex = 0; gIndex < divs && doPrint; gIndex++)
            {

            for (uint32 bIndex = 0; bIndex < divs; bIndex++)
                {

                if (linesPrinted >= gDumpLineLimit)
                    {
                    doPrint = false;
                    break;
                    }

                if (!storedPtr || (fPixelType == 1))
                    {

                    printf ("    table [%3u] [%3u] [%3u] = %6u %6u %6u\n",
                            rIndex,
                            gIndex,
                            bIndex,
                            (unsigned) samples [0],
                            (unsigned) samples [1],
                            (unsigned) samples [2]);

                    }

                else if (fPixelType == 0)
                    {

                    printf ("    table [%3u] [%3u] [%3u] = %6u %6u %6u (stored as %4u %4u %4u)\n",
                            rIndex,
                            gIndex,
                            bIndex,
                            (unsigned) samples [0],
                            (unsigned) samples [1],
                            (unsigned) samples [2],
                            (unsigned) storedPtr [0],
                            (unsigned) storedPtr [1],
                            (unsigned) storedPtr [2]);

                    storedPtr += 3;

                    }

                else                         // fPixelType == 2
                    {

                    const real32 *sPtr32 = (const real32 *) storedPtr;

                    printf ("    table [%3u] [%3u] [%3u] = %6u %6u %6u (stored as %6.4f %6.4f %6.4f)\n",
                            rIndex,
                            gIndex,
                            bIndex,
                            (unsigned) samples [0],
                            (unsigned) samples [1],
                            (unsigned) samples [2],
                            sPtr32 [0],
                            sPtr32 [1],
                            sPtr32 [2]);

                    storedPtr += (3 * 4);

                    }

                samples += 4;

                ++linesPrinted;

                } // b

            } // g

        } // r

    if (linesPrinted < divs * divs * divs)
        {

        printf ("%s... %u entries omitted ...\n",
                indentBuf,
                (divs * divs * divs) - linesPrinted);

        }

    }

#endif  // qDNGValidate

/*****************************************************************************/
/*****************************************************************************/
/*****************************************************************************/

dng_masked_rgb_tables::dng_masked_rgb_tables ()
    {

    }

/*****************************************************************************/

dng_masked_rgb_tables::dng_masked_rgb_tables
    (const std::vector<std::shared_ptr<dng_masked_rgb_table> > &tables,
     composite_method compositeMethod)

    :   fCompositeMethod (compositeMethod)

    ,   fTables (tables)

    {

    Validate ();

    }

/*****************************************************************************/

bool dng_masked_rgb_tables::IsNOP () const
    {

    if (fTables.empty ())
        {
        return true;
        }

    return false;

    }

/*****************************************************************************/

void dng_masked_rgb_tables::Validate () const
    {

    // Check number of tables.

    if (fTables.size () > kMaxTables)
        {
        ThrowBadFormat ("Too many tables in RGBTables");
        }

    // Check each table and keep track of # of tables with empty semantic
    // names.

    uint32 numEmptyNames = 0;

    for (const auto &t : fTables)
        {

        DNG_REQUIRE (t, "Invalid table pointer in RGBTables");

        t->Validate ();

        if (t->SemanticName ().IsEmpty ())
            {
            ++numEmptyNames;
            }

        }

    // There can be at most 1 table with an empty semantic name (i.e., at most
    // one background table).

    if (numEmptyNames > 1)
        {

        ThrowBadFormat ("Only one table in RGBTables can "
                        "have empty semantic name");

        }

    }

/*****************************************************************************/

void dng_masked_rgb_tables::AddDigest (dng_md5_printer &printer) const
    {

    // Header.

    printer.Process ("dng_masked_rgb_tables", 21);

    // Number of tables.

    uint32 numTables = (uint32) fTables.size ();

    printer.Process (&numTables,
                     (uint32) sizeof (numTables));

    // Process each table.

    for (const auto &t : fTables)
        {
        t->AddDigest (printer);
        }

    // Composite method.

    printer.Process (&fCompositeMethod,
                     (uint32) sizeof (fCompositeMethod));

    }

/*****************************************************************************/

void dng_masked_rgb_tables::PutStream (dng_stream &stream) const
    {

    // Write the number of tables.

    stream.Put_uint32 ((uint32) fTables.size ());

    // Write the composite method.

    stream.Put_uint32 ((uint32) fCompositeMethod);

    // Then write each table.

    for (const auto &t : fTables)
        {
        t->PutStream (stream);
        }

    }

/*****************************************************************************/

dng_masked_rgb_tables * dng_masked_rgb_tables::GetStream (dng_host &host,
                                                          dng_stream &stream,
                                                          const bool isDraft)
    {

    // Read number of tables.

    uint32 numTables = stream.Get_uint32 ();

    // A zero value is valid, which simply means the tag is empty and is a
    // NOP. Return nullptr in this case.

    if (numTables == 0)
        {
        return nullptr;
        }

    if (numTables > kMaxTables)
        {
        ThrowBadFormat ("RGBTables: numTables too large");
        }

    // In earlier DNG 1.6 drafts, there was no CompositeMethod field. The only
    // supported method was Weighted Sum.

    composite_method method = kWeightedSum;

    // In the final DNG 1.6 revision, there is a CompositeMethod field.

    if (!isDraft)
        {

        method = (composite_method) stream.Get_uint32 ();

        if (method != kWeightedSum &&
            method != kSequential)
            {
            ThrowBadFormat ("RGBTables: invalid composite method");
            }

        }

    // Read each table.

    std::vector<std::shared_ptr<dng_masked_rgb_table> > tables;

    tables.resize (numTables);

    for (auto &t : tables)
        {

        t.reset (new dng_masked_rgb_table);

        t->GetStream (host, stream);

        }

    // Done reading all the tables.

    return new dng_masked_rgb_tables (tables,
                                      method);

    }

/*****************************************************************************/

#if qDNGValidate

void dng_masked_rgb_tables::Dump () const
    {

    printf ("Number of tables: %u\n", (unsigned) fTables.size ());

    printf ("CompositeMethod: %s\n",
            UseWeightedSumMethod () ? "Weighted Sum"
                                    : "Sequential");

    for (size_t i = 0; i < fTables.size (); i++)
        {

        printf ("Table %u:\n", (unsigned) i);

        fTables [i]->Dump (4);

        }

    }

#endif  // qDNGValidate

/*****************************************************************************/
/*****************************************************************************/
/*****************************************************************************/

class dng_rgb_to_rgb_1d_function : public dng_1d_function
    {

    private:

        const dng_rgb_table &fTable;

        uint32 fPlane;

    public:

        dng_rgb_to_rgb_1d_function (const dng_rgb_table &table,
                                    uint32 plane)

            :   fTable (table)
            ,   fPlane (plane)

            {

            DNG_ASSERT (fTable.Dimensions () == 1, "1D table expected");

            }

        virtual real64 Evaluate (real64 x) const
            {

            uint32 divisions = fTable.Divisions ();

            real64 scaled = x * (real64) (divisions - 1);

            int32 index = Pin_int32 (0,
                                     (int32) scaled,
                                     divisions - 2);

            real64 fract = scaled - (real64) index;

            const uint16 *table = fTable.Samples () + (index * 4) + fPlane;

            real64 y = ((1.0 - fract) * (real64) table [0] +
                        (      fract) * (real64) table [4]) * (1.0 / 65535.0);

            return x + fTable.Amount () * (y - x);

            }

    };


/*****************************************************************************/
/*****************************************************************************/
/*****************************************************************************/

dng_rgb_to_rgb_table_data::dng_rgb_to_rgb_table_data (dng_host &host,
                                                      const dng_rgb_table &table)

    :   fTable (table)

    ,   fNeedMatrix (false)

    ,   fEncodeMatrix ()
    ,   fDecodeMatrix ()

    ,   fEncodeTable ()
    ,   fDecodeTable ()

    {

    // Find encode/decode matrix, if any.

        {

        const dng_color_space *space = NULL;

        switch (table.Primaries ())
            {

            case dng_rgb_table::primaries_sRGB:
                {
                space = &dng_space_sRGB::Get ();
                break;
                }

            case dng_rgb_table::primaries_Adobe:
                {
                space = &dng_space_AdobeRGB::Get ();
                break;
                }

            case dng_rgb_table::primaries_ProPhoto:
                {
                break;
                }

            case dng_rgb_table::primaries_P3:
                {
                space = &dng_space_DisplayP3::Get ();
                break;
                }

            case dng_rgb_table::primaries_Rec2020:
                {
                space = &dng_space_Rec2020::Get ();
                break;
                }

            default:
                {
                DNG_REPORT ("Unknown RGB table primaries");
                }

            }

        fNeedMatrix = (space != NULL);

        if (fNeedMatrix)
            {

            fEncodeMatrix = space->MatrixFromPCS () *
                            dng_space_ProPhoto::Get ().MatrixToPCS ();

            fDecodeMatrix = dng_space_ProPhoto::Get ().MatrixFromPCS () *
                            space->MatrixToPCS ();

            }

        }

    // Find encode/decode gamma tables, if any.

        {

        const dng_1d_function *gamma = NULL;

        switch (table.Gamma ())
            {

            case dng_rgb_table::gamma_Linear:
                {
                break;
                }

            case dng_rgb_table::gamma_sRGB:
                {
                gamma = &dng_function_GammaEncode_sRGB::Get ();
                break;
                }

            case dng_rgb_table::gamma_1_8:
                {
                gamma = &dng_function_GammaEncode_1_8::Get ();
                break;
                }

            case dng_rgb_table::gamma_2_2:
                {
                gamma = &dng_function_GammaEncode_2_2::Get ();
                break;
                }

            case dng_rgb_table::gamma_Rec2020:
                {
                gamma = &dng_function_GammaEncode_Rec709::Get ();
                break;
                }

            default:
                {
                DNG_REPORT ("Unknown RGB table gamma");
                }

            }

        if (fTable.Dimensions () == 1)
            {

            for (uint32 plane = 0; plane < 3; plane++)
                {

                fTable1D [plane].Reset (new dng_1d_table);

                dng_rgb_to_rgb_1d_function mapPlane (fTable,
                                                     plane);

                if (gamma == NULL)
                    {

                    fTable1D [plane]->Initialize (host.Allocator (),
                                                  mapPlane,
                                                  false);

                    }

                else
                    {

                    dng_1d_inverse inverse (*gamma);

                    dng_1d_concatenate firstPart (*gamma,
                                                  mapPlane);

                    dng_1d_concatenate combined (firstPart,
                                                 inverse);

                    fTable1D [plane]->Initialize (host.Allocator (),
                                                  combined,
                                                  false);

                    }

                }

            }

        else if (gamma != NULL)
            {

            fEncodeTable.Reset (new dng_1d_table);
            fDecodeTable.Reset (new dng_1d_table);

            fEncodeTable->Initialize (host.Allocator (),
                                      *gamma,
                                      false);

            dng_1d_inverse inverse (*gamma);

            fDecodeTable->Initialize (host.Allocator (),
                                      inverse,
                                      false);

            }

        }

    }

/*****************************************************************************/

dng_rgb_to_rgb_table_data::~dng_rgb_to_rgb_table_data ()
    {

    }

/*****************************************************************************/

void dng_rgb_to_rgb_table_data::Process_32 (dng_pixel_buffer &buffer,
                                            dng_pixel_buffer *optMaskBuffer,
                                            uint32 optMaskPlane,
                                            const dng_rect &dstArea,
                                            uint32 bufferStartPlane,
                                            const bool needOverrange)
    {

    uint32 p0 = bufferStartPlane;
    uint32 p1 = bufferStartPlane + 1;
    uint32 p2 = bufferStartPlane + 2;

    const real32 *mPtr = nullptr;

    int32 mRowStep = 0;

    if (optMaskBuffer)
        {

        mPtr = optMaskBuffer->ConstPixel_real32 (dstArea.t,
                                                 dstArea.l,
                                                 optMaskPlane);

        mRowStep = optMaskBuffer->RowStep ();

        }

    if (fTable.Dimensions () == 3)
        {

        DoRGBtoRGBTable3D (buffer.DirtyPixel_real32 (dstArea.t, dstArea.l, p0),
                           buffer.DirtyPixel_real32 (dstArea.t, dstArea.l, p1),
                           buffer.DirtyPixel_real32 (dstArea.t, dstArea.l, p2),
                           mPtr,
                           dstArea.H (),
                           dstArea.W (),
                           buffer.RowStep (),
                           mRowStep,
                           fTable.Divisions (),
                           fTable.Samples (),
                           (real32) fTable.Amount (),
                           (uint32) fTable.Gamut (),
                           fNeedMatrix ? &fEncodeMatrix : NULL,
                           fNeedMatrix ? &fDecodeMatrix : NULL,
                           fEncodeTable.Get (),
                           fDecodeTable.Get (),
                           needOverrange);

        }

    else
        {

        DoRGBtoRGBTable1D (buffer.DirtyPixel_real32 (dstArea.t, dstArea.l, p0),
                           buffer.DirtyPixel_real32 (dstArea.t, dstArea.l, p1),
                           buffer.DirtyPixel_real32 (dstArea.t, dstArea.l, p2),
                           mPtr,
                           dstArea.H (),
                           dstArea.W (),
                           buffer.RowStep (),
                           mRowStep,
                           *fTable1D [0],
                           *fTable1D [1],
                           *fTable1D [2],
                           (uint32) fTable.Gamut (),
                           fNeedMatrix ? &fEncodeMatrix : NULL,
                           fNeedMatrix ? &fDecodeMatrix : NULL,
                           needOverrange);

        }

    }

/*****************************************************************************/

void dng_rgb_to_rgb_table_data::AddDigest (dng_md5_printer &printer) const
    {

        {

        const dng_fingerprint tableFingerPrint = fTable.Fingerprint ();

        printer.Process (tableFingerPrint.data,
                         dng_fingerprint::kDNGFingerprintSize);

        }

    if (fNeedMatrix)
        {

        for (uint32 i = 0; i < 3; i++)
            {

            printer.Process (fEncodeMatrix [i], 3 * sizeof (fEncodeMatrix [i] [0]));
            printer.Process (fDecodeMatrix [i], 3 * sizeof (fEncodeMatrix [i] [0]));

            }

        }

    if (fEncodeTable.Get () && fDecodeTable.Get ())
        {

        printer.Process (fEncodeTable->Table (),
                         (2 + fEncodeTable->Count ()) * sizeof (fEncodeTable->Table () [0]));

        printer.Process (fDecodeTable->Table (),
                         (2 + fEncodeTable->Count ()) * sizeof (fEncodeTable->Table () [0]));

        }

    if (fTable.Dimensions () != 3)
        {

        for (uint32 i = 0; i < 3; i++)
            {

            printer.Process (fTable1D [i]->Table (),
                             (2 + fTable1D [i]->Count ()) * sizeof (fTable1D [i]->Table () [0]));

            }

        }

    }

/*****************************************************************************/
/*****************************************************************************/
/*****************************************************************************/

#if qDNGUseXMP

/*****************************************************************************/

class MoveBigTablesToDictionaryContext
    {

    public:

        dng_xmp &fXMP;

        dng_big_table_dictionary &fDictionary;

    public:

        MoveBigTablesToDictionaryContext (dng_xmp &xmp,
                                          dng_big_table_dictionary &dictionary)

            :   fXMP (xmp)
            ,   fDictionary (dictionary)

            {

            }

    };

/*****************************************************************************/

static bool MoveBigTablesToDictionaryCallback (const char *ns,
                                               const char *path,
                                               void *callbackData)
    {

    if (path == NULL || path [0] == 0)
        {
        return true;
        }

    dng_string s;

    s.Set (path);

    // Check if ends with Table_ and 32 characters.

    if (s.Length () < 38 ||
        strncmp (s.Get () + s.Length () - 38,
                 "Table_",
                 6) != 0)
        {
        return true;
        }

    // Make sure there is either nothing or a namespace before.

    if (s.Length () >= 39 &&
        s.Get () [s.Length () - 39] != ':')
        {
        return true;
        }

    // See if the last 32 characters are a valid fingerprint.

    char fingerString [32];

    memcpy (fingerString,
            s.Get () + s.Length () - 32,
            32);

    dng_fingerprint fingerprint;

    if (!fingerprint.FromUtf8HexString (fingerString))
        {
        return true;
        }

    if (!fingerprint.IsValid ())
        {
        return true;
        }

    MoveBigTablesToDictionaryContext *context =
                (MoveBigTablesToDictionaryContext *) callbackData;

    if (!context->fDictionary.HasTable (fingerprint))
        {

        dng_string tableString;

        if (context->fXMP.GetString (ns, path, tableString))
            {

            AutoPtr<dng_memory_block> dBlock;

            uint32 dCount = 0;

            dng_big_table::ASCIItoBinary (context->fXMP.Allocator (),
                                          tableString.Get (),
                                          tableString.Length (),
                                          dBlock,
                                          dCount);

            if (dCount)
                {

                dng_ref_counted_block bigTableBlock (dCount);

                memcpy (bigTableBlock.Buffer (),
                        dBlock->Buffer (),
                        dCount);

                context->fDictionary.AddTable (fingerprint,
                                               bigTableBlock);

                }

            }

        }

    // Now remove the big table string from the xmp.

    context->fXMP.Remove (ns, path);

    return true;

    }

/*****************************************************************************/

void MoveBigTablesToDictionary (dng_xmp &xmp,
                                dng_big_table_dictionary &dictionary)
    {

    MoveBigTablesToDictionaryContext context (xmp, dictionary);

    xmp.IteratePaths (MoveBigTablesToDictionaryCallback,
                      (void *) &context,
                      XMP_NS_CRS,
                      nullptr,
                      true);

    xmp.IteratePaths (MoveBigTablesToDictionaryCallback,
                      (void *) &context,
                      XMP_NS_CRSS,
                      nullptr,
                      true);

    }

/*****************************************************************************/

void DualParseXMP (dng_host &host,
                   dng_xmp &xmp,
                   dng_big_table_dictionary &dictionary,
                   const void *blockData,
                   uint32 blockSize)
    {

    // Null pad XMP text so we can use string searching functions.

    AutoPtr<dng_memory_block> tempBlock (host.Allocate (blockSize + 1));

    memcpy (tempBlock->Buffer (),
            blockData,
            blockSize);

    tempBlock->Buffer_uint8 () [blockSize] = 0;

    char *blockString = tempBlock->Buffer_char ();

    // Assume default namespace short names.  Since this is just
    // a performance optimization, don't worry about the rare case
    // of non-default namespace short names.

    dng_string ns_crs  ("crs:");
    dng_string ns_crss ("crss:");

    // Search for big tables in XMP using fast search.

    struct TableEntry
        {

        // Fingerprint of found big table.

        dng_fingerprint fFingerprint;

        // Offset and byte count of big table data.

        size_t          fDataOffset;
        size_t          fDataBytes;

        // Offset and byte count of entire big table tag, including wrapper.

        size_t          fWrapperOffset;
        size_t          fWrapperBytes;

        };

    std::vector<TableEntry> entries;

        {

        size_t offset = 0;

        while (const char *search = strstr (blockString + offset, "Table_"))
            {

            TableEntry entry;

            // Is the search result followed by a valid hex encoded fingerprint?

            if (search + 6 + 32 > blockString + blockSize)
                {
                break;      // Too near end of xmp block to even fit fingerprint
                }

            char fingerString [33];

            memcpy (fingerString,
                    search + 6,
                    32);

            fingerString [32] = 0;

            if (!entry.fFingerprint.FromUtf8HexString (fingerString))
                {

                // We did not find a table, so just start searching again just
                // after the "Table_" string.

                offset = (search - blockString) + 6;

                continue;   // Not followed by valid fingerprint

                }

            // Look for big tables in both the XMP_NS_CRS and XMP_NS_CRSS
            // namespaces.

            bool foundTable = false;

            for (uint32 nsIndex = 0; nsIndex < 2; nsIndex++)
                {

                dng_string ns = (nsIndex == 0 ? ns_crs : ns_crss);

                int32 nsLength = ns.Length ();

                // Look for compact mode tables.
                // Example:
                //     crs:Table_fingerprint="data"

                if (search >= blockString + nsLength + 1 &&
                    memcmp (search - nsLength, ns.Get (), nsLength) == 0 &&
                    (search [-nsLength - 1] == ' '  ||
                     search [-nsLength - 1] == '\t' ||
                     search [-nsLength - 1] == '\n' ||
                     search [-nsLength - 1] == '\r') &&
                    search + 6 + 32 + 2 < blockString + blockSize &&
                    memcmp (search + 6 + 32, "=\"", 2) == 0)
                    {

                    const char *dataStart = search + 6 + 32 + 2;

                    // Find termination

                    const char *endSearch = strstr (dataStart, "\"");

                    if (endSearch)
                        {

                        entry.fDataOffset = dataStart - blockString;
                        entry.fDataBytes  = endSearch - dataStart;

                        entry.fWrapperOffset = entry.fDataOffset - 2 - 32 - 6 - nsLength;
                        entry.fWrapperBytes  = entry.fDataBytes + nsLength + 6 + 32 + 2 + 1;

                        foundTable = true;

                        break;

                        }

                    }

                // Look for non-compact mode tables.
                // Example:
                //     <crs:Table_fingerprint>data</crs:Table_fingerprint>

                if (search >= blockString + nsLength + 1 &&
                    memcmp (search - nsLength, ns.Get (), nsLength) == 0 &&
                    search [-nsLength - 1] == '<' &&
                    search + 6 + 32 + 1 < blockString + blockSize &&
                    search [6 + 32] == '>')
                    {

                    const char *dataStart = search + 6 + 32 + 1;

                    dng_string endString ("</");

                    endString.Append (ns.Get ());
                    endString.Append ("Table_");
                    endString.Append (fingerString);
                    endString.Append (">");

                    // Find termination

                    const char *endSearch = strstr (dataStart, endString.Get ());

                    if (endSearch)
                        {

                        entry.fDataOffset = dataStart - blockString;
                        entry.fDataBytes  = endSearch - dataStart;

                        entry.fWrapperOffset = entry.fDataOffset - 2 - 32 - 6 - nsLength;
                        entry.fWrapperBytes  = entry.fDataBytes + (nsLength + 6 + 32 + 2) * 2 + 1;

                        foundTable = true;

                        break;

                        }

                    }

                }

            // We found a table, start search again after this table's wrapper.

            if (foundTable)
                {

                entries.push_back (entry);

                offset = entry.fWrapperOffset +
                         entry.fWrapperBytes;

                }

            // If we did not find a table, just start searching again just
            // after the "Table_" string.

            else
                {

                offset = (search - blockString) + 6;

                }

            }

        }

    // Did we find any tables in our scan?

    if (entries.size ())
        {

        // Add any new tables to dictionary, and remove from xmp that will be passed
        // to parser.

        size_t srcOffset = 0;
        size_t dstOffset = 0;

        for (size_t index = 0; index < entries.size (); index++)
            {

            if (!dictionary.HasTable (entries [index].fFingerprint))
                {

                AutoPtr<dng_memory_block> dBlock;

                uint32 dCount = 0;

                dng_big_table::ASCIItoBinary (host.Allocator (),
                                              ((const char *) blockData) + entries [index].fDataOffset,
                                              (uint32) entries [index].fDataBytes,
                                              dBlock,
                                              dCount);

                if (dCount)
                    {

                    dng_ref_counted_block bigTableBlock (dCount);

                    memcpy (bigTableBlock.Buffer (),
                            dBlock->Buffer (),
                            dCount);

                    dictionary.AddTable (entries [index].fFingerprint,
                                         bigTableBlock);

                    }

                }

            size_t copyBytes = entries [index].fWrapperOffset - srcOffset;

            memcpy (blockString + dstOffset,
                    ((const char *) blockData) + srcOffset,
                    copyBytes);

            dstOffset += copyBytes;

            srcOffset = entries [index].fWrapperOffset +
                        entries [index].fWrapperBytes;

            }

        // Copy bytes after last block.

        memcpy (blockString + dstOffset,
                ((const char *) blockData) + srcOffset,
                blockSize - srcOffset);

        dstOffset += blockSize - srcOffset;

        // Parse the remaining XMP with the big table blocks removed.

        xmp.Parse (host,
                   blockString,
                   (uint32) dstOffset);

        }

    // No tables founds, just parse the original XMP data.

    else
        {

        xmp.Parse (host,
                   blockData,
                   blockSize);

        }

    // Slow path, in case fast parser missed something.

    MoveBigTablesToDictionary (xmp, dictionary);

    }

/*****************************************************************************/

#endif  // qDNGUseXMP

/*****************************************************************************/
