Skip to main content
Skip table of contents

Processing images with IDOMImage

Introduction

Mako's IDOMImage class offers a convenient, format-independent tool set for handling image data.

  • An IDOMImage can be created from an input file stream, with built-in support for several image decoders - JPEG, TIFF, PNG, PSD etc. 

  • An IDOMImage can be created by rendering PDF page content (in its entirety or a section thereof) with complete control over resolution, colorspace, color management and more

  • Images on a PDF page are represented in the Mako DOM as IDOMImage objects, making them easy to work with

  • For example, the first step to adding an image to a PDF page is to create it as an IDOMImage 

  • An IDOMImage can be written to an output file stream, with built-in support for several image encoders - JPEG, TIFF, PNG etc.

Various methods provide complete access to the image data. There are APIs to return information about an image - width and height in pixels, colorspace, resolution, bits-per sample (e.g. 8- or 16-bit) and more, while others let you set the properties of an image.

The example in this KB article shows how to extract a sub-section of an image and copy it to a new image. It demonstrates the use of streams, which are a convenient and memory-efficient way to handle image data. Both C++ and C# examples are provided as the method for addressing scanline data is slightly different due to differences between the two languages. (The C# example should be followed for creating similar code for Java or Python.)

This example also makes use of a bit scaler, a filter that can be applied to an image to regularize color component sample size.

C++ sample
CPP
/* -----------------------------------------------------------------------
 * <copyright file="MakoPartialImage.cpp.cpp" company="Global Graphics Software Ltd">
 *  Copyright (c) 2022-2024 Global Graphics Software Ltd. All rights reserved.
 * </copyright>
 * <summary>
 *  This example is provided on an "as is" basis and without warranty of any kind.
 *  Global Graphics Software Ltd. does not warrant or make any representations
 *  regarding the use or results of use of this example.
 * </summary>
 * -----------------------------------------------------------------------
 */

#include <iostream>

#include <jawsmako/jawsmako.h>
#include <jawsmako/pdfoutput.h>

using namespace JawsMako;
using namespace EDL;

IDOMImagePtr getPartialImage(const IJawsMakoPtr& mako, IDOMImagePtr image, const FRect& subImageRect);

int main()
{
    try
    {
        const auto mako = IJawsMako::create();
        mako->enableAllFeatures(mako);
        const U8String testFilePath = R"(..\..\TestFiles\)";
        const IDOMImagePtr image = 
            IDOMJPEGImage::create(mako, IInputStream::createFromFile(mako, testFilePath + "WEV_086.JPG"));

        const auto partialImage = getPartialImage(mako, image, FRect(230, 230, 400, 250));
        IDOMPNGImage::encode(mako, partialImage, IOutputStream::createToFile(mako, "JustTheKayak.png"));
    }
    catch (IError& e)
    {
        const String errorFormatString = getEDLErrorString(e.getErrorCode());
        std::wcerr << L"Exception thrown: " << e.getErrorDescription(errorFormatString) << std::endl;
        return static_cast<int>(e.getErrorCode());
    }
    catch (std::exception& e)
    {
        std::wcerr << L"std::exception thrown: " << e.what() << std::endl;
        return 1;
    }

    return 0;
}

/// <summary>
 /// Extract part of an image, the area of which is described by an FRect where:
 ///   x,y are the top left corner, in pixels
 ///   dX, dY are the width and height
 /// </summary>
 /// <param name="mako"></param>
 /// <param name="image"></param>
 /// <param name="subImageRect"></param>
 /// <returns>The resulting IDOMImage</returns>
IDOMImagePtr getPartialImage(const IJawsMakoPtr& mako, IDOMImagePtr image, const FRect& subImageRect)
{
    // Get some details of the original image
    auto imageFrame = image->getImageFrame(mako);
    auto bps = imageFrame->getBPS();

    if (bps < 8)
    {
        image = IDOMFilteredImage::create(mako, image, IDOMImageBitScalerFilter::create(mako, 8));
        imageFrame = image->getImageFrame(mako);
        bps = 8;
    }
    else if (bps != 8 && bps != 16)
    {
        image = IDOMFilteredImage::create(mako, image, IDOMImageBitScalerFilter::create(mako, 16));
        imageFrame = image->getImageFrame(mako);
        bps = 16;
    }

    const auto colorSpace = imageFrame->getColorSpace();
    const auto stride = imageFrame->getRawBytesPerRow();
    const auto bpp = imageFrame->getNumChannels() * bps / 8;

    // Check the requested area is within the bounds of the original
    const auto originalImageRect = new FRect(0.0, 0.0, imageFrame->getWidth(), imageFrame->getHeight());
    if (!originalImageRect->containsRect(subImageRect))
        return {};

    IRAInputStreamPtr reader;
    IRAOutputStreamPtr writer;
    mako->getTempStore()->createTemporaryReaderWriterPair(reader, writer);
    const IInputStreamPtr inStream = IInputStream::createFromLz4Compressed(mako, reader);
    const IOutputStreamPtr outStream = IOutputStream::createToLz4Compressed(mako, writer);

    IImageFrameWriterPtr frameWriter;
    auto subImage = IDOMRawImage::createWriterAndImage(
        mako,
        frameWriter,
        colorSpace,
        static_cast<uint32>(subImageRect.dX),
        static_cast<uint32>(subImageRect.dY),
        bps,
        imageFrame->getXResolution(),
        imageFrame->getYResolution(),
        eImageExtraChannelType::eIECNone,
        inStream, outStream);

    CEDLSimpleBuffer rowBuffer(stride);

    // Move row pointer down to the first row of the partial image
    imageFrame->skipScanLines(static_cast<uint32>(subImageRect.y));

    // Copy the portion of the scanlines that we need
    for (uint32 i = 0; i < static_cast<uint32>(subImageRect.dY); i++)
    {
        imageFrame->readScanLine(&rowBuffer[0], rowBuffer.size());
        frameWriter->writeScanLine(&rowBuffer[static_cast<uint32>(subImageRect.x) * bpp]);
    }
    frameWriter->flushData();

    return subImage;
}

C# sample
C#
/* -----------------------------------------------------------------------
 * <copyright file="MakoPartialImage.cs" company="Global Graphics Software Ltd">
 *  Copyright (c) 2022-2024 Global Graphics Software Ltd. All rights reserved.
 * </copyright>
 * <summary>
 *  This example is provided on an "as is" basis and without warranty of any kind.
 *  Global Graphics Software Ltd. does not warrant or make any representations
 *  regarding the use or results of use of this example.
 * </summary>
 * -----------------------------------------------------------------------
 */

using JawsMako;

namespace MakoPartialImage;

internal class Program
{
    static void Main(string[] args)
    {
        try
        {
            var mako = IJawsMako.create();
            IJawsMako.enableAllFeatures(mako);
            var testFilePath = @"..\..\..\..\TestFiles\";
            IDOMImage image = IDOMJPEGImage.create(mako,
                IInputStream.createFromFile(mako, testFilePath + "WEV_086.JPG"));

            var partialImage = GetPartialImage(mako, image, new FRect(230, 230, 400, 250));
            IDOMPNGImage.encode(mako, partialImage, IOutputStream.createToFile(mako, "JustTheKayak.png"));
        }
        catch (MakoException e)
        {
            Console.WriteLine($"Mako exception thrown: {e.m_msg}");
        }
        catch (Exception e)
        {
            Console.WriteLine($"Exception thrown: {e}");
        }
    }

    /// <summary>
    /// Extract part of an image, the area of which is described by an FRect where:
    ///   x,y are the top left corner, in pixels
    ///   dX, dY are the width and height
    /// </summary>
    /// <param name="mako"></param>
    /// <param name="image"></param>
    /// <param name="subImageRect"></param>
    /// <returns>The resulting IDOMImage</returns>
    static IDOMImage GetPartialImage(IJawsMako mako, IDOMImage image, FRect subImageRect)
    {
        // Get some details of the original image
        var imageFrame = image.getImageFrame(mako);
        var bps = imageFrame.getBPS();

        if (bps < 8)
        {
            image = IDOMFilteredImage.create(mako, image, IDOMImageBitScalerFilter.create(mako, 8));
            imageFrame = image.getImageFrame(mako);
            bps = 8;
        }
        else if (bps != 8 && bps != 16)
        {
            image = IDOMFilteredImage.create(mako, image, IDOMImageBitScalerFilter.create(mako, 16));
            imageFrame = image.getImageFrame(mako);
            bps = 16;
        }

        var colorSpace = imageFrame.getColorSpace();
        var stride = imageFrame.getRawBytesPerRow();
        var bpp = imageFrame.getNumChannels() * bps / 8;
        
        // Check the requested area is within the bounds of the original
        var originalImageRect = new FRect(0.0, 0.0, imageFrame.getWidth(), imageFrame.getHeight());
        if (!originalImageRect.containsRect(subImageRect))
            return IDOMImage.Null();

        var (reader, writer) = mako.getTempStore().createTemporaryReaderWriterTuple();
        IInputStream inStream = IInputStream.createFromLz4Compressed(mako, reader);
        IOutputStream outStream = IOutputStream.createToLz4Compressed(mako, writer);

        var imageAndWriter = IDOMRawImage.createWriterAndImage(
            mako,
            colorSpace,
            (uint)subImageRect.dX,
            (uint)subImageRect.dY,
            bps, 
            imageFrame.getXResolution(), 
            imageFrame.getYResolution(), 
            eImageExtraChannelType.eIECNone,
            inStream, outStream);

        var subImage = imageAndWriter.domImage;
        var frameWriter = imageAndWriter.frameWriter;

        imageAndWriter.domImage = null;
        imageAndWriter.frameWriter = null;

        var rowBuffer = new byte[stride];
        var targetRowBuffer = new byte[(uint)subImageRect.dX * bpp];

        // Move row pointer down to the first row of the partial image
        imageFrame.skipScanLines((uint)subImageRect.y);

        // Copy the portion of the scanlines that we need
        for (uint i = 0; i < (uint)subImageRect.dY; i++)
        {
            imageFrame.readScanLine(rowBuffer);
            Array.Copy(rowBuffer, (uint)subImageRect.x * bpp, targetRowBuffer, 0, targetRowBuffer.Length);
            frameWriter.writeScanLine(targetRowBuffer);
        }
        frameWriter.flushData();

        return subImage;
    }
}

The image used in this example is here:

Customized image handling

The built-in TIFF input class (IDOMTiffImage) can handle multi-channel TIFFs, but cannot know what color the extra channels are supposed to represent. Adobe Photoshop addresses this problem by writing custom PHOTOSHOP tags into the TIFF to provide this information.

The following sample implements a custom TIFF reader that checks for the presence of such tags. If they are present, it uses this information to create an IDOMImage backed by a DeviceN color with the additional channels.

Custom TIFF image reader
CPP
/* -----------------------------------------------------------------------------------
 *  <copyright file="MakoImage2PDF.cpp" company="Global Graphics Software Ltd">
 *    Copyright (c) 2022-2024 Global Graphics Software Ltd. All rights reserved.
 *  </copyright>
 *  <summary>
 *    This example is provided on an "as is" basis and without warranty of any kind.
 *    Global Graphics Software Ltd. does not warrant or make any representations
 *    regarding the use or results of use of this example.
 *
 *    Simple sample application to load an  image into a new PDF using the Mako APIs.
 *  </summary>
 * -----------------------------------------------------------------------------------
*/

#include <exception>
#include <iostream>
#include <jawsmako/jawsmako.h>
#include <jawsmako/pdfoutput.h>
#include <edl/idompath.h>
#include "ExtGamutTiffLoader.h"

#ifdef _WIN32
#include <fcntl.h>
#include <corecrt_io.h>
#endif

#include <filesystem>
namespace fs = std::filesystem;

using namespace JawsMako;
using namespace EDL;

enum eImageFormat
{
    eIFJPG,
    eIFPNG,
    eIFTIF,
    eIFPSD
};

// Determine the associated format for a given path from the file extension
static eImageFormat imageFormatFromPath(const String& sPath)
{
    const auto path = fs::path(sPath);
    if (path.has_extension())
    {
        std::string extension = path.extension().string();
        std::transform(extension.begin(), extension.end(), extension.begin(), tolower);

        if (extension == ".jpg" || extension == ".jpeg")
            return eIFJPG;
        if (extension == ".png")
            return eIFPNG;
        if (extension == ".tif" || extension == ".tiff")
            return eIFTIF;
        if (extension == ".psd")
            return eIFPSD;
    }
    std::string message("Unsupported file type for path ");
    message += StringToU8String(sPath).c_str();
    throw std::invalid_argument(message);
}

// Image type identifier string
static U8String imageType(eImageFormat imgF)
{
	switch(imgF)
	{
    case eIFJPG:
        return "jpeg";
    case eIFPNG:
        return "png";
    case eIFTIF:
        return "tiff";
    case eIFPSD:
        return "psd";
	}
    return "";
}

#ifdef _WIN32
int wmain(int argc, wchar_t* argv[])
{
    _setmode(_fileno(stderr), _O_U16TEXT);
    _setmaxstdio(2048);
#else
int main(int argc, char* argv[])
{
#endif

    try
    {
        // Create our JawsMako instance.
        const IJawsMakoPtr jawsMako = IJawsMako::create();
        IJawsMako::enableAllFeatures(jawsMako);

        // There should be at least one argument, a source file
        if (argc < 2)
        {
            std::wcout << "Mako Image to PDF v1.2.0\n" << std::endl;
            std::wcout << "Usage:" << argv[0] << " <source file.jpg|.png|.tif|.psd> [<output file.pdf>]" << std::endl;
            return 1;
        }

        // Most JawsMako APIs can take UTF8 or Wide Character paths.
        String sInputFilePath;
        fs::path pOutputFilePath;
#ifdef _WIN32
        sInputFilePath = String(argv[1]);
#else
        sInputFilePath = U8StringToString(argv[1]);
#endif
        const auto imageFormat = imageFormatFromPath(sInputFilePath);
        pOutputFilePath = argc > 2 ? fs::path(argv[2]) : fs::path(sInputFilePath).replace_extension(".pdf");

        // Modify filename to include a marker for the image format that it was converted from
        auto modFilename = pOutputFilePath.stem();
        modFilename += "_";
        modFilename += imageType(imageFormat);
        modFilename += ".pdf";

        auto sOutputFilePath = U8String(pOutputFilePath.replace_filename(modFilename).u8string());
    	
        // Report progress
        std::wcout << L"Processing \'" << sInputFilePath << L"\'..." << std::endl;

        // Load the image from the local file
        IDOMImagePtr image;
        switch (imageFormat)
        {
        case eIFJPG:
            image = IDOMJPEGImage::create(jawsMako, IInputStream::createFromFile(jawsMako, sInputFilePath));
            break;
        case eIFPNG:
            image = IDOMPNGImage::create(jawsMako, IInputStream::createFromFile(jawsMako, sInputFilePath));
            break;
        case eIFTIF:
            image = ExtGamutTiffLoader::load(jawsMako, sInputFilePath);
            break;
        case eIFPSD:
            image = IDOMPSDImage::create(jawsMako, IInputStream::createFromFile(jawsMako, sInputFilePath));
            break;
        }

        // Get image attributes
        const IImageFramePtr imageFrame = image->getImageFrame(jawsMako);
        uint8_t numChannels = imageFrame->getNumChannels();
        const bool hasAlphaChan = imageFrame->getHasAlphaChannel();
        if (hasAlphaChan)
            numChannels--;
        const double width = imageFrame->getWidth() / imageFrame->getXResolution() * 96.0;
        const double height = imageFrame->getHeight() / imageFrame->getYResolution() * 96.0;

#if(0)
    	IDOMFilteredImagePtr filteredImage;
		switch (noOfChannels)
        {
        case 1:
        {
            // Create a filter that will convert the image to DeviceGray
            const IDOMImageColorSpaceSubstitutionFilterPtr colorSpaceSubstitutionFilter = IDOMImageColorSpaceSubstitutionFilter::create(jawsMako, IDOMColorSpaceDeviceGray::create(jawsMako));
            filteredImage = IDOMFilteredImage::create(jawsMako, image, colorSpaceSubstitutionFilter);
        }
        break;

        case 3:
        {
            // Create a filter that will convert the image to DeviceRGB
            const IDOMImageColorSpaceSubstitutionFilterPtr colorSpaceSubstitutionFilter = IDOMImageColorSpaceSubstitutionFilter::create(jawsMako, IDOMColorSpaceDeviceRGB::create(jawsMako));
            filteredImage = IDOMFilteredImage::create(jawsMako, image, colorSpaceSubstitutionFilter);
        }
        break;

        case 4:
        {
            // Create a filter that will convert the image to DeviceCMYK
            const IDOMImageColorSpaceSubstitutionFilterPtr colorSpaceSubstitutionFilter = IDOMImageColorSpaceSubstitutionFilter::create(jawsMako, IDOMColorSpaceDeviceCMYK::create(jawsMako));
            filteredImage = IDOMFilteredImage::create(jawsMako, image, colorSpaceSubstitutionFilter);
        }
        break;

        default:
            std::cout << inputFilePath << " is not a 1-, 3- or 4-channel image. Expecting an 8-bit grayscale, RGB or CMYK image." << std::endl;
            return 1;
        }
        image = filteredImage;
#endif

    	// Convert CMYK JPEG to RGB
        if (imageFormat == eIFJPG && numChannels == 4)
        {
            CEDLVector<IDOMImageFilterPtr> invertAndConvert;
            invertAndConvert.append(IDOMImageInverterFilter::create(jawsMako));
            invertAndConvert.append(IDOMImageColorConverterFilter::create(jawsMako, IDOMColorSpaceDeviceRGB::create(jawsMako), eRelativeColorimetric, eBPCDefault));
            image = IDOMFilteredImage::create(jawsMako, image, invertAndConvert);
            numChannels = 3;
        }
    	
        // Create an empty assembly, document, and page.
        IDocumentAssemblyPtr assembly = IDocumentAssembly::create(jawsMako);
        IDocumentPtr document = IDocument::create(jawsMako);
        IPagePtr page = IPage::create(jawsMako);

        // Add the page to the document, and the document to the assembly
        document->appendPage(page);
        assembly->appendDocument(document);

        // Create a fixed page that matches the image size
        // Mako units are in 96ths of an inch.
        IDOMFixedPagePtr fixedPage = IDOMFixedPage::create(jawsMako, width, height);

        // Position the image on the page, with no margin
        const FRect imageBounds(
            0.0, 0.0,         // origin (x, y)
            width, height);   // width and height

        // Create a path directly from the image
        const IDOMPathNodePtr path = IDOMPathNode::createImage(jawsMako, image, imageBounds);

        // And add the path to the page
        fixedPage->appendChild(path);

        // This becomes the page contents
        page->setContent(fixedPage);

        // Now we can write this to an PDF
        IPDFOutputPtr pdfOutput = IPDFOutput::create(jawsMako);
        switch (numChannels)
        {
        case 1:
            pdfOutput->setParameter("TargetColorSpace", "DeviceGray");
            break;
        case 3:
            pdfOutput->setParameter("TargetColorSpace", "DeviceRGB");
            break;
        case 4:
            pdfOutput->setParameter("TargetColorSpace", "DeviceCMYK");
            break;
        default:;
        }
        pdfOutput->setPreferredGrayImageCompression(IPDFOutput::eICFlate);
        pdfOutput->setPreferredColorImageCompression(IPDFOutput::eICFlate);
        pdfOutput->writeAssembly(assembly, sOutputFilePath);

    	std::wcout << L"Output file '";
        std::wcerr << U8StringToString(sOutputFilePath);
		std::wcout << L"' written.";
		std::wcerr << std::endl;
    }
    catch (IError& e)
    {
        const String errorFormatString = getEDLErrorString(e.getErrorCode());
        std::wcerr << L"Exception thrown: " << e.getErrorDescription(errorFormatString) << std::endl;
        return static_cast<int>(e.getErrorCode());
    }
    catch (std::exception& e)
    {
        std::wcerr << L"std::exception thrown: " << e.what() << std::endl;
        return 1;
    }

    return 0;
}
CPP
/* -----------------------------------------------------------------------
 * <copyright file="ExtGamutTiffLoader.h" company="Global Graphics Software Ltd">
 *  Copyright (c) 2024 Global Graphics Software Ltd. All rights reserved.
 * </copyright>
 * <summary>
 *  This example is provided on an "as is" basis and without warranty of any kind.
 *  Global Graphics Software Ltd. does not warrant or make any representations
 *  regarding the use or results of use of this example.
 * </summary>
 * -----------------------------------------------------------------------
 */

#pragma once

#include <iostream>
#include <jawsmako/jawsmako.h>
#include <jawsmako/pdfoutput.h>
#include <edl/edlvector.h>
#include <edl/edlstring.h>
#include <edl/edlsimplebuffer.h>
#include <edl/platform.h>
#include <edl/idompath.h>
#include <tiff.h>
#include <tiffconf.h>
#include <tiffio.h>


using namespace JawsMako;
using namespace EDL;

class ExtGamutTiffLoader
{
public:
    static IDOMImagePtr load(const IJawsMakoPtr& mako, const String& tiffFile);

private:
    // See Appendix A in Photoshop API Guide.pdf.
    typedef struct
    {
        uint16_t colorSpace;
        uint16_t color[4];
        uint16_t opacity;
        uint8_t kind;
    } DisplayInfo;

    static void skipBytes(uint8_t*& ptr, uint32_t n, uint8_t*& end);
    static uint8_t read8(uint8_t*& ptr, uint8_t*& end);
    static uint16_t read16(uint8_t* ptr);
    static uint16_t read16(uint8_t*& ptr, uint8_t*& end);
    static uint32_t read32(uint8_t* ptr);
    static uint32_t read32(uint8_t*& ptr, uint8_t*& end);
    static RawString readPascalString(uint8_t*& ptr, uint8_t*& end);
    static U8String readUnicodeString(uint8_t*& ptr, uint8_t*& end);
    static DisplayInfo readDisplayInfo(uint8_t*& ptr, uint8_t*& end);
    static IDOMColorSpaceDeviceNPtr createDeviceNCMYKSpace(const IJawsMakoPtr& mako,
                                                           const CRawStringVect& colorantNames,
                                                           const CEDLVector<DisplayInfo>& displayInfos);
};
CPP
/* -----------------------------------------------------------------------
 * <copyright file="ExtGamutTiffLoader.cpp" company="Global Graphics Software Ltd">
 *  Copyright (c) 2024 Global Graphics Software Ltd. All rights reserved.
 * </copyright>
 * <summary>
 *  This example is provided on an "as is" basis and without warranty of any kind.
 *  Global Graphics Software Ltd. does not warrant or make any representations
 *  regarding the use or results of use of this example.
 * </summary>
 * -----------------------------------------------------------------------
 */

#include "ExtGamutTiffLoader.h"

using namespace JawsMako;
using namespace EDL;

IDOMImagePtr ExtGamutTiffLoader::load(const IJawsMakoPtr& mako, const String& tiffFile)
{
    try
    {
        // Open the TIFF
        TIFF* tif = TIFFOpen(StringToU8String(tiffFile).c_str(), "r");
        if (!tif)
            throw std::runtime_error("Unable to open TIFF");

        uint32_t width, height;
        TIFFGetField(tif, TIFFTAG_IMAGEWIDTH, &width);
        TIFFGetField(tif, TIFFTAG_IMAGELENGTH, &height);
        uint32_t tagLength;

        uint8_t* tagBufferPtr = nullptr;
        TIFFGetField(tif, TIFFTAG_PHOTOSHOP, &tagLength, &tagBufferPtr);
        if (tagLength == 0)
        {
            TIFFClose(tif);
            return IDOMTIFFImage::create(mako, IInputStream::createFromFile(mako, tiffFile));
        }

        CRawStringVect channelNames;
        CU8StringVect unicodeChannelNames;
        CEDLVector<DisplayInfo> displayInfos;

        auto tagEnd = tagBufferPtr + tagLength;
        while (tagBufferPtr < tagEnd)
        {
            const auto signature = read32(tagBufferPtr, tagEnd);
            if (signature != 0x3842494D) // "8BIM"
            {
                throw std::runtime_error("Invalid Photoshop signature");
            }

            const auto imageResourceId = read16(tagBufferPtr, tagEnd);

            // Pad if not even and add the byte for the length.
            auto nameLength = *tagBufferPtr;
            nameLength += ((nameLength & 1) ^ 1) + 1;

            // Skip the name.
            skipBytes(tagBufferPtr, nameLength, tagEnd);

            auto resourceDataLength = read32(tagBufferPtr, tagEnd);
            const auto resourceDataPtr = tagBufferPtr;

            switch (imageResourceId)
            {
            case 0x03EE:
            {
                // Names of the alpha channels as a series of Pascal strings.
                auto alphaChannelPtr = resourceDataPtr;
                auto alphaChannelEnd = alphaChannelPtr + resourceDataLength;
                while (alphaChannelPtr < alphaChannelEnd)
                {
                    channelNames.append(readPascalString(alphaChannelPtr, alphaChannelEnd));
                }
            }
            break;

            case 0x0415:
            {
                // Unicode Alpha Names
                auto alphaNamesPtr = resourceDataPtr;
                auto alphaNamesEnd = alphaNamesPtr + resourceDataLength;
                while (alphaNamesPtr < alphaNamesEnd)
                {
                    unicodeChannelNames.append(readUnicodeString(alphaNamesPtr, alphaNamesEnd));
                }
            }
            break;

            case 0x0435:
            {
                // DisplayInfo structure to support floating point colours.
                auto displayInfoPtr = resourceDataPtr;
                auto displayInfoEnd = displayInfoPtr + resourceDataLength;

                // Skip undocumented 4 bytes at the start of the data.
                skipBytes(displayInfoPtr, 4, displayInfoEnd);

                while (displayInfoPtr < displayInfoEnd)
                {
                    displayInfos.append(readDisplayInfo(displayInfoPtr, displayInfoEnd));
                }
            }
            break;

            default:
                ;
            }

            // Pad to even and advance.
            resourceDataLength += resourceDataLength & 1;
            skipBytes(tagBufferPtr, resourceDataLength, tagEnd);
        }

        TIFFClose(tif);

        const uint32_t numChannels = displayInfos.size();
        if (numChannels == 0)
            return IDOMTIFFImage::create(mako, IInputStream::createFromFile(mako, tiffFile));

        if (numChannels != channelNames.size() && numChannels != unicodeChannelNames.size())
            throw std::runtime_error("Invalid number of channels");

        // First test the colour space.
        const auto image = IDOMTIFFImage::create(mako, IInputStream::createFromFile(mako, tiffFile), 0, true);
        const auto deviceN = edlobj2IDOMColorSpaceDeviceN(image->getImageFrame(mako)->getColorSpace());

        // We only support DeviceCMYK or a 4 component ICCBased space.
        if (!deviceN || numChannels + 4 != deviceN->getNumComponents())
        {
            throw std::runtime_error("Invalid color space");
        }

        const auto colorSpaceType = deviceN->getAlternateColorSpace()->getColorSpaceType();
        if (colorSpaceType != IDOMColorSpace::eDeviceCMYK && colorSpaceType != IDOMColorSpace::eICCBased)
        {
            throw std::runtime_error("Invalid color space");
        }

        // Create a new DeviceN/CMYK color space from the Photoshop info.
        // We'll use the channel names if we can, or the unicode names if not.
        const auto colorSpace = createDeviceNCMYKSpace(mako, !channelNames.empty() ? channelNames : unicodeChannelNames, displayInfos);

        // The spot channels are stored such that we need to invert them.
        // We can do this easily by using a IDOMImageChannelSelectorFilter, which will extract
        // each channel as a DeviceGray image, and then recombining them with the DeviceN color
        // space we've created.  But as the CMYK channels do not need to be inverted we also
        // need to use a IDOMImageInverterFilter so that they are recombined back correctly.
        CEDLVector<IDOMImagePtr> images;
        for (uint8_t i = 0; i < 4; i++)
        {
            CEDLVector<IDOMImageFilterPtr> filters;
            filters.append(IDOMImageChannelSelectorFilter::create(mako, IDOMImageChannelSelectorFilter::eSelectChannel, i));
            filters.append(IDOMImageInverterFilter::create(mako));
            images.append(IDOMFilteredImage::create(mako, image, filters));
        }

        for (uint8_t i = 0; i < numChannels; i++)
        {
            images.append(IDOMFilteredImage::create(mako, image,
                IDOMImageChannelSelectorFilter::create(mako,
                    IDOMImageChannelSelectorFilter::eSelectChannel, 4 + i)));
        }

        // Recombine.
        return IDOMRecombineImage::create(mako, colorSpace, images);
    }
    catch (IError& e)
    {
        const String errorFormatString = getEDLErrorString(e.getErrorCode());
        std::wcerr << L"Exception thrown: " << e.getErrorDescription(errorFormatString) << std::endl;
        return {};
    }
    catch (std::runtime_error& e)
    {
        std::wcerr << L"std::runtime_error thrown: " << e.what() << std::endl;
        return {};
    }
}

void ExtGamutTiffLoader::skipBytes(uint8_t*& ptr, uint32_t n, uint8_t*& end)
{
    if (ptr + n > end)
        throw std::runtime_error("Invalid Photoshop resource");

    ptr += n;
}

uint8_t ExtGamutTiffLoader::read8(uint8_t*& ptr, uint8_t*& end)
{
    if (ptr + 1 > end)
        throw std::runtime_error("Invalid Photoshop resource");

    return *ptr++;
}

uint16_t ExtGamutTiffLoader::read16(uint8_t* ptr)
{
#if EDLENDIAN == eEndianLittle  // NOLINT(clang-diagnostic-undef)
    return (ptr[0] << 8) | ptr[1];  // NOLINT(clang-diagnostic-implicit-int-conversion)
#else
    return (ptr[1] << 8) | ptr[0];
#endif
}

uint16_t ExtGamutTiffLoader::read16(uint8_t*& ptr, uint8_t*& end)
{
    if (ptr + 2 > end)
        throw std::runtime_error("Invalid photoshop resource");

    ptr += 2;

    return read16(ptr - 2);
}

uint32_t ExtGamutTiffLoader::read32(uint8_t* ptr)
{
#if EDLENDIAN == eEndianLittle  // NOLINT(clang-diagnostic-undef)
    return (ptr[0] << 24) | (ptr[1] << 16) | (ptr[2] << 8) | ptr[3];
#else
    return (ptr[3] << 24) | (ptr[2] << 16) | (ptr[1] << 8) | ptr[0];
#endif
}

uint32_t ExtGamutTiffLoader::read32(uint8_t*& ptr, uint8_t*& end)
{
    if (ptr + 4 > end)
        throw std::runtime_error("Invalid photoshop resource");

    ptr += 4;

    return read32(ptr - 4);
}

RawString ExtGamutTiffLoader::readPascalString(uint8_t*& ptr, uint8_t*& end)
{
    const auto length = *ptr;

    if (ptr + length + 1 > end)
        throw std::runtime_error("Invalid photoshop resource");

    ptr += (length + 1);

    return RawString(reinterpret_cast<char*>(ptr) - length, length);
}

U8String ExtGamutTiffLoader::readUnicodeString(uint8_t*& ptr, uint8_t*& end)
{
    auto length = read32(ptr, end);

    // The string should be null terminated.
    // Test the string terminator.
    if ((ptr + (length * sizeof(u16char)) > end) || reinterpret_cast<u16char*>(ptr)[length - 1] != 0x0000)
        throw std::runtime_error("Invalid photoshop resource");

    U16String unicodeString;
    unicodeString.reserve(length);

    for (uint32_t i = 0; i < length; i++)
    {
        unicodeString += static_cast<u16char>(read16(ptr));
        ptr += sizeof(u16char);
    }

    return U16StringToU8String(unicodeString);
}

ExtGamutTiffLoader::DisplayInfo ExtGamutTiffLoader::readDisplayInfo(uint8_t*& ptr, uint8_t*& end)
{
    DisplayInfo displayInfo;

    displayInfo.colorSpace = read16(ptr, end);

    for (unsigned short& i : displayInfo.color)
        i = read16(ptr, end);

    displayInfo.opacity = read16(ptr, end);
    displayInfo.kind = read8(ptr, end);

    return displayInfo;
}

IDOMColorSpaceDeviceNPtr ExtGamutTiffLoader::createDeviceNCMYKSpace(const IJawsMakoPtr& mako, const CRawStringVect& colorantNames, const CEDLVector<ExtGamutTiffLoader::DisplayInfo>& displayInfos)
{
    // Build up our new colorants, starting with the the base colourants
    auto colorants = IDOMColorSpaceDeviceN::CColorantInfoVect();
    colorants.append({
        IDOMColorSpaceDeviceN::CColorantInfo{ "Cyan",    { 1.0, 0.0, 0.0, 0.0 } },
        IDOMColorSpaceDeviceN::CColorantInfo{ "Magenta", { 0.0, 1.0, 0.0, 0.0 } },
        IDOMColorSpaceDeviceN::CColorantInfo{ "Yellow",  { 0.0, 0.0, 1.0, 0.0 } },
        IDOMColorSpaceDeviceN::CColorantInfo{ "Black",   { 0.0, 0.0, 0.0, 1.0 } }
    });

    // Now any spot colorants.
    for (uint32_t i = 0; i < colorantNames.size(); i++)
    {
        const auto& displayInfo = displayInfos[i];
        const auto& colorantName = colorantNames[i];

        // Is it a spot?  The Photoshop API Guide only documents 1 or 0,
        // but looking online suggests that a spot channel will have a value of 2.
        if (displayInfos[i].kind == 2)
        {
            switch (displayInfos[i].colorSpace)
            {
            case 0:
            {
                // RGB.  The first three values in the color data are red, green, and blue.
                // They are full unsigned 16-bit values as in Apple's RGBColor data
                // structure. Pure red = 65535, 0, 0.
                const auto color = IDOMColor::createSolidRgb(mako,
                    static_cast<float>(displayInfo.color[0]) / 65535.0f,
                    static_cast<float>(displayInfo.color[1]) / 65535.0f,
                    static_cast<float>(displayInfo.color[2]) / 65535.0f);
                color->setColorSpace(IDOMColorSpaceDeviceCMYK::create(mako), eRelativeColorimetric, eBPCDefault, mako);

                colorants.append(
                    IDOMColorSpaceDeviceN::CColorantInfo(
                        colorantName,
                        CEDLVector<double>({
                            color->getComponentValue(0),
                            color->getComponentValue(1),
                            color->getComponentValue(2),
                            color->getComponentValue(3)
                        })
                    )
                );
            }
            break;

            case 2:

                // CMYK.  The four values in the color data are cyan, magenta, yellow, and
                // black.  They are full unsigned 16-bit values. 0 = 100 % ink.  Pure
                // cyan = 0, 65535, 65535, 65535.
                colorants.append(
                    IDOMColorSpaceDeviceN::CColorantInfo(
                        colorantName,
                        CEDLVector<double>({
                            1.0 - (displayInfo.color[0] / 65535.0),
                            1.0 - (displayInfo.color[1] / 65535.0),
                            1.0 - (displayInfo.color[2] / 65535.0),
                            1.0 - (displayInfo.color[3] / 65535.0)
                        })
                    )
                );
                break;

                // Currently unsupported.
#if 0
            case 1:
                // HSV. The first three values in the color data are hue, saturation, and
                // brightness.  They are full unsigned 16-bit values as in Apple's
                // HSVColor data structure.  Pure red = 0, 65535, 65535.
                break;

            case 7:
                // Lab.  The first three values in the color data are lightness, a chrominance,
                // and b chrominance.  Lightness is a 16-bit value from 0...10000.  The
                // chromanance components are each 16-bit values from -12800...12700.  Gray
                // values are represented by chrominance components of 0.
                // Pure white = 10000, 0, 0.
                break;

                // Grayscale.   The first value in the color data is the gray value, from 0...10000.
            case 8:
                break;
#endif

            default:
                throw std::runtime_error("Unsupported DisplayInfo color space");
            }
        }
    }

    // Create a new DeviceN color space using the colorants.
    return IDOMColorSpaceDeviceN::create(mako, colorants, IDOMColorSpaceDeviceCMYK::create(mako));
}

JavaScript errors detected

Please note, these errors can depend on your browser setup.

If this problem persists, please contact our support.