//========================================================================
//
// GfxState.cc
//
// Copyright 1996-2003 Glyph & Cog, LLC
//
//========================================================================

//========================================================================
//
// Modified under the Poppler project - http://poppler.freedesktop.org
//
// All changes made under the Poppler project to this file are licensed
// under GPL version 2 or later
//
// Copyright (C) 2005 Kristian Høgsberg <krh@redhat.com>
// Copyright (C) 2006, 2007 Jeff Muizelaar <jeff@infidigm.net>
// Copyright (C) 2006, 2010 Carlos Garcia Campos <carlosgc@gnome.org>
// Copyright (C) 2006-2022, 2024-2026 Albert Astals Cid <aacid@kde.org>
// Copyright (C) 2009, 2012 Koji Otani <sho@bbr.jp>
// Copyright (C) 2009, 2011-2016, 2020, 2023 Thomas Freitag <Thomas.Freitag@alfa.de>
// Copyright (C) 2009, 2019 Christian Persch <chpe@gnome.org>
// Copyright (C) 2010 Paweł Wiejacha <pawel.wiejacha@gmail.com>
// Copyright (C) 2010 Christian Feuersänger <cfeuersaenger@googlemail.com>
// Copyright (C) 2011 Andrea Canciani <ranma42@gmail.com>
// Copyright (C) 2012, 2020 William Bader <williambader@hotmail.com>
// Copyright (C) 2013 Lu Wang <coolwanglu@gmail.com>
// Copyright (C) 2013 Hib Eris <hib@hiberis.nl>
// Copyright (C) 2013 Fabio D'Urso <fabiodurso@hotmail.it>
// Copyright (C) 2015, 2020 Adrian Johnson <ajohnson@redneon.com>
// Copyright (C) 2016 Marek Kasik <mkasik@redhat.com>
// Copyright (C) 2017, 2019, 2022 Oliver Sander <oliver.sander@tu-dresden.de>
// Copyright (C) 2018 Klarälvdalens Datakonsult AB, a KDAB Group company, <info@kdab.com>. Work sponsored by the LiMux project of the city of Munich
// Copyright (C) 2018 Volker Krause <vkrause@kde.org>
// Copyright (C) 2018, 2019 Adam Reichold <adam.reichold@t-online.de>
// Copyright (C) 2019 LE GARREC Vincent <legarrec.vincent@gmail.com>
// Copyright (C) 2020, 2021, 2026 Philipp Knechtges <philipp-dev@knechtges.com>
// Copyright (C) 2020 Lluís Batlle i Rossell <viric@viric.name>
// Copyright (C) 2024 Athul Raj Kollareth <krathul3152@gmail.com>
// Copyright (C) 2024 Nelson Benítez León <nbenitezl@gmail.com>
// Copyright (C) 2025, 2026 g10 Code GmbH, Author: Sune Stolborg Vuorela <sune@vuorela.dk>
// Copyright (C) 2025 Trystan Mata <trystan.mata@tytanium.xyz>
// Copyright (C) 2025 Arnav V <arnav0872@gmail.com>
// Copyright (C) 2026 Adam Sampson <ats@offog.org>
// Copyright (C) 2026 Stefan Brüns <stefan.bruens@rwth-aachen.de>
//
// To see a description of the changes please see the Changelog file that
// came with your tarball or type make ChangeLog if you are building from git
//
//========================================================================

#include <config.h>

#include <algorithm>
#include <memory>
#include <cstddef>
#include <cmath>
#include <cstring>
#include "goo/gmem.h"
#include "Error.h"
#include "Object.h"
#include "Array.h"
#include "Page.h"
#include "Gfx.h"
#include "GfxState.h"
#include "GfxState_helpers.h"
#include "GfxFont.h"
#include "GlobalParams.h"
#include "OutputDev.h"
#include "Stream.h"
#include "splash/SplashTypes.h"

//------------------------------------------------------------------------

// Max depth of nested color spaces.  This is used to catch infinite
// loops in the color space object structure.
constexpr int colorSpaceRecursionLimit = 8;

//------------------------------------------------------------------------

bool Matrix::invertTo(Matrix *other) const
{
    const double det_denominator = determinant();
    if (unlikely(det_denominator == 0)) {
        *other = { 1, 0, 0, 1, 0, 0 };
        return false;
    }

    const double det = 1 / det_denominator;
    other->m[0] = m[3] * det;
    other->m[1] = -m[1] * det;
    other->m[2] = -m[2] * det;
    other->m[3] = m[0] * det;
    other->m[4] = (m[2] * m[5] - m[3] * m[4]) * det;
    other->m[5] = (m[1] * m[4] - m[0] * m[5]) * det;

    return true;
}

void Matrix::translate(double tx, double ty)
{
    double x0 = tx * m[0] + ty * m[2] + m[4];
    double y0 = tx * m[1] + ty * m[3] + m[5];
    m[4] = x0;
    m[5] = y0;
}

void Matrix::scale(double sx, double sy)
{
    m[0] *= sx;
    m[1] *= sx;
    m[2] *= sy;
    m[3] *= sy;
}

void Matrix::transform(double x, double y, double *tx, double *ty) const
{
    double temp_x, temp_y;

    temp_x = m[0] * x + m[2] * y + m[4];
    temp_y = m[1] * x + m[3] * y + m[5];

    *tx = temp_x;
    *ty = temp_y;
}

// Matrix norm, taken from _cairo_matrix_transformed_circle_major_axis
double Matrix::norm() const
{
    double f, g, h, i, j;

    i = m[0] * m[0] + m[1] * m[1];
    j = m[2] * m[2] + m[3] * m[3];

    f = 0.5 * (i + j);
    g = 0.5 * (i - j);
    h = m[0] * m[2] + m[1] * m[3];

    return sqrt(f + hypot(g, h));
}

//------------------------------------------------------------------------

struct GfxBlendModeInfo
{
    const char *name;
    GfxBlendMode mode;
};

static const GfxBlendModeInfo gfxBlendModeNames[] = { { .name = "Normal", .mode = gfxBlendNormal },         { .name = "Compatible", .mode = gfxBlendNormal },
                                                      { .name = "Multiply", .mode = gfxBlendMultiply },     { .name = "Screen", .mode = gfxBlendScreen },
                                                      { .name = "Overlay", .mode = gfxBlendOverlay },       { .name = "Darken", .mode = gfxBlendDarken },
                                                      { .name = "Lighten", .mode = gfxBlendLighten },       { .name = "ColorDodge", .mode = gfxBlendColorDodge },
                                                      { .name = "ColorBurn", .mode = gfxBlendColorBurn },   { .name = "HardLight", .mode = gfxBlendHardLight },
                                                      { .name = "SoftLight", .mode = gfxBlendSoftLight },   { .name = "Difference", .mode = gfxBlendDifference },
                                                      { .name = "Exclusion", .mode = gfxBlendExclusion },   { .name = "Hue", .mode = gfxBlendHue },
                                                      { .name = "Saturation", .mode = gfxBlendSaturation }, { .name = "Color", .mode = gfxBlendColor },
                                                      { .name = "Luminosity", .mode = gfxBlendLuminosity } };

#define nGfxBlendModeNames ((int)((sizeof(gfxBlendModeNames) / sizeof(GfxBlendModeInfo))))

//------------------------------------------------------------------------
//
// NB: This must match the GfxColorSpaceMode enum defined in
// GfxState.h
static const char *gfxColorSpaceModeNames[] = { "DeviceGray", "CalGray", "DeviceRGB", "CalRGB", "DeviceCMYK", "Lab", "ICCBased", "Indexed", "Separation", "DeviceN", "Pattern", "DeviceRGBA" };

#define nGfxColorSpaceModes ((sizeof(gfxColorSpaceModeNames) / sizeof(char *)))

#if USE_CMS

static const std::map<unsigned int, unsigned int>::size_type CMSCACHE_LIMIT = 2048;

#    include <lcms2.h>
#    define LCMS_FLAGS (cmsFLAGS_NOOPTIMIZE | cmsFLAGS_BLACKPOINTCOMPENSATION)

static void lcmsprofiledeleter(void *profile)
{
    cmsCloseProfile(profile);
}

GfxLCMSProfilePtr make_GfxLCMSProfilePtr(void *profile)
{
    if (profile == nullptr) {
        return GfxLCMSProfilePtr();
    }
    return GfxLCMSProfilePtr(profile, lcmsprofiledeleter);
}

void GfxColorTransform::doTransform(void *in, void *out, unsigned int size)
{
    cmsDoTransform(transform, in, out, size);
}

// transformA should be a cmsHTRANSFORM
GfxColorTransform::GfxColorTransform(void *transformA, int cmsIntentA, unsigned int inputPixelTypeA, unsigned int transformPixelTypeA)
{
    transform = transformA;
    cmsIntent = cmsIntentA;
    inputPixelType = inputPixelTypeA;
    transformPixelType = transformPixelTypeA;
}

GfxColorTransform::~GfxColorTransform()
{
    cmsDeleteTransform(transform);
}

// convert color space signature to cmsColor type
static unsigned int getCMSColorSpaceType(cmsColorSpaceSignature cs);
static unsigned int getCMSNChannels(cmsColorSpaceSignature cs);

#endif

//------------------------------------------------------------------------
// GfxColorSpace
//------------------------------------------------------------------------

GfxColorSpace::GfxColorSpace()
{
    overprintMask = 0x0f;
}

GfxColorSpace::~GfxColorSpace() = default;

std::unique_ptr<GfxColorSpace> GfxColorSpace::parse(GfxResources *res, Object *csObj, OutputDev *out, GfxState *state, int recursion)
{
    Object obj1;

    if (recursion > colorSpaceRecursionLimit) {
        error(errSyntaxError, -1, "Loop detected in color space objects");
        return {};
    }

    if (csObj->isName()) {
        const std::string &csName = csObj->getNameString();
        if (csName == "DeviceGray" || csName == "G") {
            if (res != nullptr) {
                Object objCS = res->lookupColorSpace("DefaultGray");
                if (objCS.isNull()) {
                    return state->copyDefaultGrayColorSpace();
                }
                return GfxColorSpace::parse(nullptr, &objCS, out, state);
            }
            return state->copyDefaultGrayColorSpace();
        }
        if (csName == "DeviceRGB" || csName == "RGB") {
            if (res != nullptr) {
                Object objCS = res->lookupColorSpace("DefaultRGB");
                if (objCS.isNull()) {
                    return state->copyDefaultRGBColorSpace();
                }
                return GfxColorSpace::parse(nullptr, &objCS, out, state);
            }
            return state->copyDefaultRGBColorSpace();
        }
        if (csName == "DeviceCMYK" || csName == "CMYK") {
            if (res != nullptr) {
                Object objCS = res->lookupColorSpace("DefaultCMYK");
                if (objCS.isNull()) {
                    return state->copyDefaultCMYKColorSpace();
                }
                return GfxColorSpace::parse(nullptr, &objCS, out, state);
            }
            return state->copyDefaultCMYKColorSpace();
        }
        if (csName == "Pattern") {
            return std::make_unique<GfxPatternColorSpace>(nullptr);
        }
        error(errSyntaxWarning, -1, "Bad color space '{0:r}'", &csName);

    } else if (csObj->isArrayOfLengthAtLeast(1)) {
        obj1 = csObj->arrayGet(0);
        if (obj1.isName("DeviceGray") || obj1.isName("G")) {
            if (res != nullptr) {
                Object objCS = res->lookupColorSpace("DefaultGray");
                if (objCS.isNull()) {
                    return state->copyDefaultGrayColorSpace();
                }
                return GfxColorSpace::parse(nullptr, &objCS, out, state);
            }
            return state->copyDefaultGrayColorSpace();
        }
        if (obj1.isName("DeviceRGB") || obj1.isName("RGB")) {
            if (res != nullptr) {
                Object objCS = res->lookupColorSpace("DefaultRGB");
                if (objCS.isNull()) {
                    return state->copyDefaultRGBColorSpace();
                }
                return GfxColorSpace::parse(nullptr, &objCS, out, state);
            }
            return state->copyDefaultRGBColorSpace();
        }
        if (obj1.isName("DeviceCMYK") || obj1.isName("CMYK")) {
            if (res != nullptr) {
                Object objCS = res->lookupColorSpace("DefaultCMYK");
                if (objCS.isNull()) {
                    return state->copyDefaultCMYKColorSpace();
                }
                return GfxColorSpace::parse(nullptr, &objCS, out, state);
            }
            return state->copyDefaultCMYKColorSpace();
        }
        if (obj1.isName("CalGray")) {
            return GfxCalGrayColorSpace::parse(*csObj->getArray(), state);
        }
        if (obj1.isName("CalRGB")) {
            return GfxCalRGBColorSpace::parse(*csObj->getArray(), state);
        }
        if (obj1.isName("Lab")) {
            return GfxLabColorSpace::parse(*csObj->getArray(), state);
        }
        if (obj1.isName("ICCBased")) {
            return GfxICCBasedColorSpace::parse(*csObj->getArray(), out, state, recursion);
        }
        if (obj1.isName("Indexed") || obj1.isName("I")) {
            return GfxIndexedColorSpace::parse(res, *csObj->getArray(), out, state, recursion);
        }
        if (obj1.isName("Separation")) {
            return GfxSeparationColorSpace::parse(res, *csObj->getArray(), out, state, recursion);
        }
        if (obj1.isName("DeviceN")) {
            return GfxDeviceNColorSpace::parse(res, *csObj->getArray(), out, state, recursion);
        }
        if (obj1.isName("Pattern")) {
            return GfxPatternColorSpace::parse(res, *csObj->getArray(), out, state, recursion);
        }
        error(errSyntaxWarning, -1, "Bad color space");

    } else if (csObj->isDict()) {
        obj1 = csObj->dictLookup("ColorSpace");
        if (obj1.isName("DeviceGray")) {
            if (res != nullptr) {
                Object objCS = res->lookupColorSpace("DefaultGray");
                if (objCS.isNull()) {
                    return state->copyDefaultGrayColorSpace();
                }
                return GfxColorSpace::parse(nullptr, &objCS, out, state);
            }
            return state->copyDefaultGrayColorSpace();
        }
        if (obj1.isName("DeviceRGB")) {
            if (res != nullptr) {
                Object objCS = res->lookupColorSpace("DefaultRGB");
                if (objCS.isNull()) {
                    return state->copyDefaultRGBColorSpace();
                }
                return GfxColorSpace::parse(nullptr, &objCS, out, state);
            }
            return state->copyDefaultRGBColorSpace();
        }
        if (obj1.isName("DeviceCMYK")) {
            if (res != nullptr) {
                Object objCS = res->lookupColorSpace("DefaultCMYK");
                if (objCS.isNull()) {
                    return state->copyDefaultCMYKColorSpace();
                }
                return GfxColorSpace::parse(nullptr, &objCS, out, state);
            }
            return state->copyDefaultCMYKColorSpace();
        }
        error(errSyntaxWarning, -1, "Bad color space dict'");

    } else {
        error(errSyntaxWarning, -1, "Bad color space - expected name or array or dict");
    }
    return {};
}

void GfxColorSpace::createMapping(std::vector<std::unique_ptr<GfxSeparationColorSpace>> * /*separationList*/, size_t /*maxSepComps*/) { }

void GfxColorSpace::getDefaultRanges(double *decodeLow, double *decodeRange, int /*maxImgPixel*/) const
{
    int i;

    for (i = 0; i < getNComps(); ++i) {
        decodeLow[i] = 0;
        decodeRange[i] = 1;
    }
}

int GfxColorSpace::getNumColorSpaceModes()
{
    return nGfxColorSpaceModes;
}

const char *GfxColorSpace::getColorSpaceModeName(int idx)
{
    return gfxColorSpaceModeNames[idx];
}

#if USE_CMS

static void CMSError(cmsContext /*contextId*/, cmsUInt32Number /*ecode*/, const char *text)
{
    error(errSyntaxWarning, -1, "{0:s}", text);
}

static void setCMSErrorHandler()
{
    static bool installed = false;

    if (!installed) {
        cmsSetLogErrorHandler(CMSError);
        installed = true;
    }
}

unsigned int getCMSColorSpaceType(cmsColorSpaceSignature cs)
{
    switch (cs) {
    case cmsSigXYZData:
        return PT_XYZ;
        break;
    case cmsSigLabData:
        return PT_Lab;
        break;
    case cmsSigLuvData:
        return PT_YUV;
        break;
    case cmsSigYCbCrData:
        return PT_YCbCr;
        break;
    case cmsSigYxyData:
        return PT_Yxy;
        break;
    case cmsSigRgbData:
        return PT_RGB;
        break;
    case cmsSigGrayData:
        return PT_GRAY;
        break;
    case cmsSigHsvData:
        return PT_HSV;
        break;
    case cmsSigHlsData:
        return PT_HLS;
        break;
    case cmsSigCmykData:
        return PT_CMYK;
        break;
    case cmsSigCmyData:
        return PT_CMY;
        break;
    case cmsSig2colorData:
    case cmsSig3colorData:
    case cmsSig4colorData:
    case cmsSig5colorData:
    case cmsSig6colorData:
    case cmsSig7colorData:
    case cmsSig8colorData:
    case cmsSig9colorData:
    case cmsSig10colorData:
    case cmsSig11colorData:
    case cmsSig12colorData:
    case cmsSig13colorData:
    case cmsSig14colorData:
    case cmsSig15colorData:
    default:
        break;
    }
    return PT_RGB;
}

unsigned int getCMSNChannels(cmsColorSpaceSignature cs)
{
    switch (cs) {
    case cmsSigXYZData:
    case cmsSigLuvData:
    case cmsSigLabData:
    case cmsSigYCbCrData:
    case cmsSigYxyData:
    case cmsSigRgbData:
    case cmsSigHsvData:
    case cmsSigHlsData:
    case cmsSigCmyData:
    case cmsSig3colorData:
        return 3;
        break;
    case cmsSigGrayData:
        return 1;
        break;
    case cmsSigCmykData:
    case cmsSig4colorData:
        return 4;
        break;
    case cmsSig2colorData:
        return 2;
        break;
    case cmsSig5colorData:
        return 5;
        break;
    case cmsSig6colorData:
        return 6;
        break;
    case cmsSig7colorData:
        return 7;
        break;
    case cmsSig8colorData:
        return 8;
        break;
    case cmsSig9colorData:
        return 9;
        break;
    case cmsSig10colorData:
        return 10;
        break;
    case cmsSig11colorData:
        return 11;
        break;
    case cmsSig12colorData:
        return 12;
        break;
    case cmsSig13colorData:
        return 13;
        break;
    case cmsSig14colorData:
        return 14;
        break;
    case cmsSig15colorData:
        return 15;
    default:
        break;
    }
    return 3;
}
#endif

//------------------------------------------------------------------------
// GfxDeviceGrayColorSpace
//------------------------------------------------------------------------

GfxDeviceGrayColorSpace::GfxDeviceGrayColorSpace() = default;

GfxDeviceGrayColorSpace::~GfxDeviceGrayColorSpace() = default;

std::unique_ptr<GfxColorSpace> GfxDeviceGrayColorSpace::copy() const
{
    return std::make_unique<GfxDeviceGrayColorSpace>();
}

void GfxDeviceGrayColorSpace::getGray(const GfxColor &color, GfxGray *gray) const
{
    *gray = clip01(color.c[0]);
}

void GfxDeviceGrayColorSpace::getGrayLine(unsigned char *in, unsigned char *out, int length)
{
    memcpy(out, in, length);
}

void GfxDeviceGrayColorSpace::getRGB(const GfxColor &color, GfxRGB *rgb) const
{
    rgb->r = rgb->g = rgb->b = clip01(color.c[0]);
}

void GfxDeviceGrayColorSpace::getRGBLine(unsigned char *in, unsigned int *out, int length)
{
    int i;

    for (i = 0; i < length; i++) {
        out[i] = (in[i] << 16) | (in[i] << 8) | (in[i] << 0);
    }
}

void GfxDeviceGrayColorSpace::getRGBLine(unsigned char *in, unsigned char *out, int length)
{
    for (int i = 0; i < length; i++) {
        *out++ = in[i];
        *out++ = in[i];
        *out++ = in[i];
    }
}

void GfxDeviceGrayColorSpace::getRGBXLine(unsigned char *in, unsigned char *out, int length)
{
    for (int i = 0; i < length; i++) {
        *out++ = in[i];
        *out++ = in[i];
        *out++ = in[i];
        *out++ = 255;
    }
}

void GfxDeviceGrayColorSpace::getCMYKLine(unsigned char *in, unsigned char *out, int length)
{
    for (int i = 0; i < length; i++) {
        *out++ = 0;
        *out++ = 0;
        *out++ = 0;
        *out++ = in[i];
    }
}

void GfxDeviceGrayColorSpace::getDeviceNLine(unsigned char *in, unsigned char *out, int length)
{
    for (int i = 0; i < length; i++) {
        for (int j = 0; j < SPOT_NCOMPS + 4; j++) {
            out[j] = 0;
        }
        out[4] = in[i];
        out += (SPOT_NCOMPS + 4);
    }
}

void GfxDeviceGrayColorSpace::getCMYK(const GfxColor &color, GfxCMYK *cmyk) const
{
    cmyk->c = cmyk->m = cmyk->y = 0;
    cmyk->k = clip01(gfxColorComp1 - color.c[0]);
}

void GfxDeviceGrayColorSpace::getDeviceN(const GfxColor &color, GfxColor *deviceN) const
{
    clearGfxColor(deviceN);
    deviceN->c[3] = clip01(gfxColorComp1 - color.c[0]);
}

void GfxDeviceGrayColorSpace::getDefaultColor(GfxColor *color) const
{
    color->c[0] = 0;
}

//------------------------------------------------------------------------
// GfxCalGrayColorSpace
//------------------------------------------------------------------------

GfxCalGrayColorSpace::GfxCalGrayColorSpace()
{
    whiteX = whiteY = whiteZ = 1;
    blackX = blackY = blackZ = 0;
    gamma = 1;
}

GfxCalGrayColorSpace::~GfxCalGrayColorSpace() = default;

std::unique_ptr<GfxColorSpace> GfxCalGrayColorSpace::copy() const
{
    auto cs = std::make_unique<GfxCalGrayColorSpace>();
    cs->whiteX = whiteX;
    cs->whiteY = whiteY;
    cs->whiteZ = whiteZ;
    cs->blackX = blackX;
    cs->blackY = blackY;
    cs->blackZ = blackZ;
    cs->gamma = gamma;
#if USE_CMS
    cs->transform = transform;
#endif
    return cs;
}

// This is the inverse of MatrixLMN in Example 4.10 from the PostScript
// Language Reference, Third Edition.
static const double xyzrgb[3][3] = { { 3.240449, -1.537136, -0.498531 }, { -0.969265, 1.876011, 0.041556 }, { 0.055643, -0.204026, 1.057229 } };

// From the same reference as above, the inverse of the DecodeLMN function.
// This is essentially the gamma function of the sRGB profile.
static double srgb_gamma_function(double x)
{
    // 0.04045 is what lcms2 uses, but the PS Reference Example 4.10 specifies 0.03928???
    // if (x <= 0.04045 / 12.92321) {
    if (x <= 0.03928 / 12.92321) {
        return x * 12.92321;
    }
    return 1.055 * pow(x, 1.0 / 2.4) - 0.055;
}

// D65 is the white point of the sRGB profile as it is specified above in the xyzrgb array
static const double white_d65_X = 0.9505;
static const double white_d65_Y = 1.0;
static const double white_d65_Z = 1.0890;

#if USE_CMS
// D50 is the default white point as used in ICC profiles and in the lcms2 library
static const double white_d50_X = 0.96422;
static const double white_d50_Y = 1.0;
static const double white_d50_Z = 0.82521;

static void inline bradford_transform_to_d50(double &X, double &Y, double &Z, const double source_whiteX, const double source_whiteY, const double source_whiteZ)
{
    if (source_whiteX == white_d50_X && source_whiteY == white_d50_Y && source_whiteZ == white_d50_Z) {
        // early exit if noop
        return;
    }
    // at first apply Bradford matrix
    double rho_in = 0.8951000 * X + 0.2664000 * Y - 0.1614000 * Z;
    double gamma_in = -0.7502000 * X + 1.7135000 * Y + 0.0367000 * Z;
    double beta_in = 0.0389000 * X - 0.0685000 * Y + 1.0296000 * Z;

    // apply a diagonal matrix with the diagonal entries being the inverse bradford-transformed white point
    rho_in /= 0.8951000 * source_whiteX + 0.2664000 * source_whiteY - 0.1614000 * source_whiteZ;
    gamma_in /= -0.7502000 * source_whiteX + 1.7135000 * source_whiteY + 0.0367000 * source_whiteZ;
    beta_in /= 0.0389000 * source_whiteX - 0.0685000 * source_whiteY + 1.0296000 * source_whiteZ;

    // now revert the two steps above, but substituting the source white point by the device white point (D50)
    // Since the white point is known a priori this has been combined into a single operation.
    X = 0.98332566 * rho_in - 0.15005819 * gamma_in + 0.13095252 * beta_in;
    Y = 0.43069901 * rho_in + 0.52894900 * gamma_in + 0.04035199 * beta_in;
    Z = 0.00849698 * rho_in + 0.04086079 * gamma_in + 0.79284618 * beta_in;
}
#endif

static void inline bradford_transform_to_d65(double &X, double &Y, double &Z, const double source_whiteX, const double source_whiteY, const double source_whiteZ)
{
    if (source_whiteX == white_d65_X && source_whiteY == white_d65_Y && source_whiteZ == white_d65_Z) {
        // early exit if noop
        return;
    }
    // at first apply Bradford matrix
    double rho_in = 0.8951000 * X + 0.2664000 * Y - 0.1614000 * Z;
    double gamma_in = -0.7502000 * X + 1.7135000 * Y + 0.0367000 * Z;
    double beta_in = 0.0389000 * X - 0.0685000 * Y + 1.0296000 * Z;

    // apply a diagonal matrix with the diagonal entries being the inverse bradford-transformed white point
    rho_in /= 0.8951000 * source_whiteX + 0.2664000 * source_whiteY - 0.1614000 * source_whiteZ;
    gamma_in /= -0.7502000 * source_whiteX + 1.7135000 * source_whiteY + 0.0367000 * source_whiteZ;
    beta_in /= 0.0389000 * source_whiteX - 0.0685000 * source_whiteY + 1.0296000 * source_whiteZ;

    // now revert the two steps above, but substituting the source white point by the device white point (D65)
    // Since the white point is known a priori this has been combined into a single operation.
    X = 0.92918329 * rho_in - 0.15299782 * gamma_in + 0.17428453 * beta_in;
    Y = 0.40698452 * rho_in + 0.53931108 * gamma_in + 0.05370440 * beta_in;
    Z = -0.00802913 * rho_in + 0.04166125 * gamma_in + 1.05519788 * beta_in;
}

std::unique_ptr<GfxColorSpace> GfxCalGrayColorSpace::parse(const Array &arr, GfxState *state)
{
    Object obj1, obj2;

    obj1 = arr.get(1);
    if (!obj1.isDict()) {
        error(errSyntaxWarning, -1, "Bad CalGray color space");
        return {};
    }
    auto cs = std::make_unique<GfxCalGrayColorSpace>();
    obj2 = obj1.dictLookup("WhitePoint");
    if (obj2.isArrayOfLength(3)) {
        cs->whiteX = obj2.arrayGet(0).getNumWithDefaultValue(1);
        cs->whiteY = obj2.arrayGet(1).getNumWithDefaultValue(1);
        cs->whiteZ = obj2.arrayGet(2).getNumWithDefaultValue(1);
    }
    obj2 = obj1.dictLookup("BlackPoint");
    if (obj2.isArrayOfLength(3)) {
        cs->blackX = obj2.arrayGet(0).getNumWithDefaultValue(0);
        cs->blackY = obj2.arrayGet(1).getNumWithDefaultValue(0);
        cs->blackZ = obj2.arrayGet(2).getNumWithDefaultValue(0);
    }

    cs->gamma = obj1.dictLookup("Gamma").getNumWithDefaultValue(1);

#if USE_CMS
    cs->transform = (state != nullptr) ? state->getXYZ2DisplayTransform() : nullptr;
#else
    (void)state;
#endif
    return cs;
}

// convert CalGray to media XYZ color space
void GfxCalGrayColorSpace::getXYZ(const GfxColor &color, double *pX, double *pY, double *pZ) const
{
    const double A = colToDbl(color.c[0]);
    const double xyzColor = pow(A, gamma);
    *pX = whiteX * xyzColor;
    *pY = whiteY * xyzColor;
    *pZ = whiteZ * xyzColor;
}

void GfxCalGrayColorSpace::getGray(const GfxColor &color, GfxGray *gray) const
{
    GfxRGB rgb;

#if USE_CMS
    if (transform && transform->getTransformPixelType() == PT_GRAY) {
        unsigned char out[gfxColorMaxComps];
        double in[gfxColorMaxComps];
        double X, Y, Z;

        getXYZ(color, &X, &Y, &Z);
        bradford_transform_to_d50(X, Y, Z, whiteX, whiteY, whiteZ);
        in[0] = X;
        in[1] = Y;
        in[2] = Z;
        transform->doTransform(in, out, 1);
        *gray = byteToCol(out[0]);
        return;
    }
#endif
    getRGB(color, &rgb);
    *gray = clip01(static_cast<GfxColorComp>(0.299 * rgb.r + 0.587 * rgb.g + 0.114 * rgb.b + 0.5));
}

void GfxCalGrayColorSpace::getRGB(const GfxColor &color, GfxRGB *rgb) const
{
    double X, Y, Z;
    double r, g, b;

    getXYZ(color, &X, &Y, &Z);
#if USE_CMS
    if (transform && transform->getTransformPixelType() == PT_RGB) {
        unsigned char out[gfxColorMaxComps];
        double in[gfxColorMaxComps];

        bradford_transform_to_d50(X, Y, Z, whiteX, whiteY, whiteZ);
        in[0] = X;
        in[1] = Y;
        in[2] = Z;
        transform->doTransform(in, out, 1);
        rgb->r = byteToCol(out[0]);
        rgb->g = byteToCol(out[1]);
        rgb->b = byteToCol(out[2]);
        return;
    }
#endif
    bradford_transform_to_d65(X, Y, Z, whiteX, whiteY, whiteZ);
    // convert XYZ to RGB, including gamut mapping and gamma correction
    r = xyzrgb[0][0] * X + xyzrgb[0][1] * Y + xyzrgb[0][2] * Z;
    g = xyzrgb[1][0] * X + xyzrgb[1][1] * Y + xyzrgb[1][2] * Z;
    b = xyzrgb[2][0] * X + xyzrgb[2][1] * Y + xyzrgb[2][2] * Z;
    rgb->r = dblToCol(srgb_gamma_function(clip01(r)));
    rgb->g = dblToCol(srgb_gamma_function(clip01(g)));
    rgb->b = dblToCol(srgb_gamma_function(clip01(b)));
}

void GfxCalGrayColorSpace::getCMYK(const GfxColor &color, GfxCMYK *cmyk) const
{
    GfxRGB rgb;
    GfxColorComp c, m, y, k;

#if USE_CMS
    if (transform && transform->getTransformPixelType() == PT_CMYK) {
        double in[gfxColorMaxComps];
        unsigned char out[gfxColorMaxComps];
        double X, Y, Z;

        getXYZ(color, &X, &Y, &Z);
        bradford_transform_to_d50(X, Y, Z, whiteX, whiteY, whiteZ);
        in[0] = X;
        in[1] = Y;
        in[2] = Z;
        transform->doTransform(in, out, 1);
        cmyk->c = byteToCol(out[0]);
        cmyk->m = byteToCol(out[1]);
        cmyk->y = byteToCol(out[2]);
        cmyk->k = byteToCol(out[3]);
        return;
    }
#endif
    getRGB(color, &rgb);
    c = clip01(gfxColorComp1 - rgb.r);
    m = clip01(gfxColorComp1 - rgb.g);
    y = clip01(gfxColorComp1 - rgb.b);
    k = c;
    if (m < k) {
        k = m;
    }
    if (y < k) {
        k = y;
    }
    cmyk->c = c - k;
    cmyk->m = m - k;
    cmyk->y = y - k;
    cmyk->k = k;
}

void GfxCalGrayColorSpace::getDeviceN(const GfxColor &color, GfxColor *deviceN) const
{
    GfxCMYK cmyk;
    clearGfxColor(deviceN);
    getCMYK(color, &cmyk);
    deviceN->c[0] = cmyk.c;
    deviceN->c[1] = cmyk.m;
    deviceN->c[2] = cmyk.y;
    deviceN->c[3] = cmyk.k;
}

void GfxCalGrayColorSpace::getDefaultColor(GfxColor *color) const
{
    color->c[0] = 0;
}

//------------------------------------------------------------------------
// GfxDeviceRGBColorSpace
//------------------------------------------------------------------------

GfxDeviceRGBColorSpace::GfxDeviceRGBColorSpace() = default;

GfxDeviceRGBColorSpace::~GfxDeviceRGBColorSpace() = default;

std::unique_ptr<GfxColorSpace> GfxDeviceRGBColorSpace::copy() const
{
    return std::make_unique<GfxDeviceRGBColorSpace>();
}

void GfxDeviceRGBColorSpace::getGray(const GfxColor &color, GfxGray *gray) const
{
    *gray = clip01(static_cast<GfxColorComp>(0.3 * color.c[0] + 0.59 * color.c[1] + 0.11 * color.c[2] + 0.5));
}

void GfxDeviceRGBColorSpace::getGrayLine(unsigned char *in, unsigned char *out, int length)
{
    int i;

    for (i = 0; i < length; i++) {
        out[i] = (in[i * 3 + 0] * 19595 + in[i * 3 + 1] * 38469 + in[i * 3 + 2] * 7472) / 65536;
    }
}

void GfxDeviceRGBColorSpace::getRGB(const GfxColor &color, GfxRGB *rgb) const
{
    rgb->r = clip01(color.c[0]);
    rgb->g = clip01(color.c[1]);
    rgb->b = clip01(color.c[2]);
}

void GfxDeviceRGBColorSpace::getRGBLine(unsigned char *in, unsigned int *out, int length)
{
    unsigned char *p;
    int i;

    for (i = 0, p = in; i < length; i++, p += 3) {
        out[i] = (p[0] << 16) | (p[1] << 8) | (p[2] << 0);
    }
}

void GfxDeviceRGBColorSpace::getRGBLine(unsigned char *in, unsigned char *out, int length)
{
    for (int i = 0; i < length; i++) {
        *out++ = *in++;
        *out++ = *in++;
        *out++ = *in++;
    }
}

void GfxDeviceRGBColorSpace::getRGBXLine(unsigned char *in, unsigned char *out, int length)
{
    for (int i = 0; i < length; i++) {
        *out++ = *in++;
        *out++ = *in++;
        *out++ = *in++;
        *out++ = 255;
    }
}

void GfxDeviceRGBColorSpace::getCMYKLine(unsigned char *in, unsigned char *out, int length)
{
    GfxColorComp c, m, y, k;

    for (int i = 0; i < length; i++) {
        c = byteToCol(255 - *in++);
        m = byteToCol(255 - *in++);
        y = byteToCol(255 - *in++);
        k = c;
        if (m < k) {
            k = m;
        }
        if (y < k) {
            k = y;
        }
        *out++ = colToByte(c - k);
        *out++ = colToByte(m - k);
        *out++ = colToByte(y - k);
        *out++ = colToByte(k);
    }
}

void GfxDeviceRGBColorSpace::getDeviceNLine(unsigned char *in, unsigned char *out, int length)
{
    GfxColorComp c, m, y, k;

    for (int i = 0; i < length; i++) {
        for (int j = 0; j < SPOT_NCOMPS + 4; j++) {
            out[j] = 0;
        }
        c = byteToCol(255 - *in++);
        m = byteToCol(255 - *in++);
        y = byteToCol(255 - *in++);
        k = c;
        if (m < k) {
            k = m;
        }
        if (y < k) {
            k = y;
        }
        out[0] = colToByte(c - k);
        out[1] = colToByte(m - k);
        out[2] = colToByte(y - k);
        out[3] = colToByte(k);
        out += (SPOT_NCOMPS + 4);
    }
}

void GfxDeviceRGBColorSpace::getCMYK(const GfxColor &color, GfxCMYK *cmyk) const
{
    GfxColorComp c, m, y, k;

    c = clip01(gfxColorComp1 - color.c[0]);
    m = clip01(gfxColorComp1 - color.c[1]);
    y = clip01(gfxColorComp1 - color.c[2]);
    k = c;
    if (m < k) {
        k = m;
    }
    if (y < k) {
        k = y;
    }
    cmyk->c = c - k;
    cmyk->m = m - k;
    cmyk->y = y - k;
    cmyk->k = k;
}

void GfxDeviceRGBColorSpace::getDeviceN(const GfxColor &color, GfxColor *deviceN) const
{
    GfxCMYK cmyk;
    clearGfxColor(deviceN);
    getCMYK(color, &cmyk);
    deviceN->c[0] = cmyk.c;
    deviceN->c[1] = cmyk.m;
    deviceN->c[2] = cmyk.y;
    deviceN->c[3] = cmyk.k;
}

void GfxDeviceRGBColorSpace::getDefaultColor(GfxColor *color) const
{
    color->c[0] = 0;
    color->c[1] = 0;
    color->c[2] = 0;
}

//------------------------------------------------------------------------
// GfxDeviceRGBAColorSpace
//------------------------------------------------------------------------

GfxDeviceRGBAColorSpace::GfxDeviceRGBAColorSpace() = default;

GfxDeviceRGBAColorSpace::~GfxDeviceRGBAColorSpace() = default;

std::unique_ptr<GfxColorSpace> GfxDeviceRGBAColorSpace::copy() const
{
    return std::make_unique<GfxDeviceRGBAColorSpace>();
}

void GfxDeviceRGBAColorSpace::getARGBPremultipliedLine(unsigned char *in, unsigned int *out, int length)
{
    unsigned char *p;
    int i;

    // Conversion from 'in' RGBA to 'out' ARGB32_PREMULTIPLIED (used by Cairo)
    for (i = 0, p = in; i < length; i++, p += 4) {
        // This applies alpha component p[3] to each RGB values (using bitwise division)
        // so final result in out[i] is ARGB32 with premultiplied alpha.
        out[i] = p[3] << 24 | (p[0] * p[3] >> 8) << 16 | (p[1] * p[3] >> 8) << 8 | (p[2] * p[3] >> 8) << 0;
    }
}

//------------------------------------------------------------------------
// GfxCalRGBColorSpace
//------------------------------------------------------------------------

GfxCalRGBColorSpace::GfxCalRGBColorSpace()
{
    whiteX = whiteY = whiteZ = 1;
    blackX = blackY = blackZ = 0;
    gammaR = gammaG = gammaB = 1;
    mat[0] = 1;
    mat[1] = 0;
    mat[2] = 0;
    mat[3] = 0;
    mat[4] = 1;
    mat[5] = 0;
    mat[6] = 0;
    mat[7] = 0;
    mat[8] = 1;
}

GfxCalRGBColorSpace::~GfxCalRGBColorSpace() = default;

std::unique_ptr<GfxColorSpace> GfxCalRGBColorSpace::copy() const
{
    auto cs = std::make_unique<GfxCalRGBColorSpace>();
    cs->whiteX = whiteX;
    cs->whiteY = whiteY;
    cs->whiteZ = whiteZ;
    cs->blackX = blackX;
    cs->blackY = blackY;
    cs->blackZ = blackZ;
    cs->gammaR = gammaR;
    cs->gammaG = gammaG;
    cs->gammaB = gammaB;
    cs->mat = mat;
#if USE_CMS
    cs->transform = transform;
#endif
    return cs;
}

std::unique_ptr<GfxColorSpace> GfxCalRGBColorSpace::parse(const Array &arr, GfxState *state)
{
    Object obj1, obj2;
    int i;

    obj1 = arr.get(1);
    if (!obj1.isDict()) {
        error(errSyntaxWarning, -1, "Bad CalRGB color space");
        return {};
    }
    auto cs = std::make_unique<GfxCalRGBColorSpace>();
    obj2 = obj1.dictLookup("WhitePoint");
    if (obj2.isArrayOfLength(3)) {
        cs->whiteX = obj2.arrayGet(0).getNumWithDefaultValue(1);
        cs->whiteY = obj2.arrayGet(1).getNumWithDefaultValue(1);
        cs->whiteZ = obj2.arrayGet(2).getNumWithDefaultValue(1);
    }
    obj2 = obj1.dictLookup("BlackPoint");
    if (obj2.isArrayOfLength(3)) {
        cs->blackX = obj2.arrayGet(0).getNumWithDefaultValue(0);
        cs->blackY = obj2.arrayGet(1).getNumWithDefaultValue(0);
        cs->blackZ = obj2.arrayGet(2).getNumWithDefaultValue(0);
    }
    obj2 = obj1.dictLookup("Gamma");
    if (obj2.isArrayOfLength(3)) {
        cs->gammaR = obj2.arrayGet(0).getNumWithDefaultValue(1);
        cs->gammaG = obj2.arrayGet(1).getNumWithDefaultValue(1);
        cs->gammaB = obj2.arrayGet(2).getNumWithDefaultValue(1);
    }
    obj2 = obj1.dictLookup("Matrix");
    if (obj2.isArrayOfLength(9)) {
        for (i = 0; i < 9; ++i) {
            Object obj3 = obj2.arrayGet(i);
            if (likely(obj3.isNum())) {
                cs->mat[i] = obj3.getNum();
            }
        }
    }

#if USE_CMS
    cs->transform = (state != nullptr) ? state->getXYZ2DisplayTransform() : nullptr;
#else
    (void)state;
#endif
    return cs;
}

// convert CalRGB to XYZ color space
void GfxCalRGBColorSpace::getXYZ(const GfxColor &color, double *pX, double *pY, double *pZ) const
{
    double A, B, C;

    A = pow(colToDbl(color.c[0]), gammaR);
    B = pow(colToDbl(color.c[1]), gammaG);
    C = pow(colToDbl(color.c[2]), gammaB);
    *pX = mat[0] * A + mat[3] * B + mat[6] * C;
    *pY = mat[1] * A + mat[4] * B + mat[7] * C;
    *pZ = mat[2] * A + mat[5] * B + mat[8] * C;
}

void GfxCalRGBColorSpace::getGray(const GfxColor &color, GfxGray *gray) const
{
    GfxRGB rgb;

#if USE_CMS
    if (transform != nullptr && transform->getTransformPixelType() == PT_GRAY) {
        unsigned char out[gfxColorMaxComps];
        double in[gfxColorMaxComps];
        double X, Y, Z;

        getXYZ(color, &X, &Y, &Z);
        bradford_transform_to_d50(X, Y, Z, whiteX, whiteY, whiteZ);
        in[0] = X;
        in[1] = Y;
        in[2] = Z;
        transform->doTransform(in, out, 1);
        *gray = byteToCol(out[0]);
        return;
    }
#endif
    getRGB(color, &rgb);
    *gray = clip01(static_cast<GfxColorComp>(0.299 * rgb.r + 0.587 * rgb.g + 0.114 * rgb.b + 0.5));
}

void GfxCalRGBColorSpace::getRGB(const GfxColor &color, GfxRGB *rgb) const
{
    double X, Y, Z;
    double r, g, b;

    getXYZ(color, &X, &Y, &Z);
#if USE_CMS
    if (transform != nullptr && transform->getTransformPixelType() == PT_RGB) {
        unsigned char out[gfxColorMaxComps];
        double in[gfxColorMaxComps];

        bradford_transform_to_d50(X, Y, Z, whiteX, whiteY, whiteZ);
        in[0] = X;
        in[1] = Y;
        in[2] = Z;
        transform->doTransform(in, out, 1);
        rgb->r = byteToCol(out[0]);
        rgb->g = byteToCol(out[1]);
        rgb->b = byteToCol(out[2]);

        return;
    }
#endif
    bradford_transform_to_d65(X, Y, Z, whiteX, whiteY, whiteZ);
    // convert XYZ to RGB, including gamut mapping and gamma correction
    r = xyzrgb[0][0] * X + xyzrgb[0][1] * Y + xyzrgb[0][2] * Z;
    g = xyzrgb[1][0] * X + xyzrgb[1][1] * Y + xyzrgb[1][2] * Z;
    b = xyzrgb[2][0] * X + xyzrgb[2][1] * Y + xyzrgb[2][2] * Z;
    rgb->r = dblToCol(srgb_gamma_function(clip01(r)));
    rgb->g = dblToCol(srgb_gamma_function(clip01(g)));
    rgb->b = dblToCol(srgb_gamma_function(clip01(b)));
}

void GfxCalRGBColorSpace::getCMYK(const GfxColor &color, GfxCMYK *cmyk) const
{
    GfxRGB rgb;
    GfxColorComp c, m, y, k;

#if USE_CMS
    if (transform != nullptr && transform->getTransformPixelType() == PT_CMYK) {
        double in[gfxColorMaxComps];
        unsigned char out[gfxColorMaxComps];
        double X, Y, Z;

        getXYZ(color, &X, &Y, &Z);
        bradford_transform_to_d50(X, Y, Z, whiteX, whiteY, whiteZ);
        in[0] = X;
        in[1] = Y;
        in[2] = Z;
        transform->doTransform(in, out, 1);
        cmyk->c = byteToCol(out[0]);
        cmyk->m = byteToCol(out[1]);
        cmyk->y = byteToCol(out[2]);
        cmyk->k = byteToCol(out[3]);
        return;
    }
#endif
    getRGB(color, &rgb);
    c = clip01(gfxColorComp1 - rgb.r);
    m = clip01(gfxColorComp1 - rgb.g);
    y = clip01(gfxColorComp1 - rgb.b);
    k = c;
    if (m < k) {
        k = m;
    }
    if (y < k) {
        k = y;
    }
    cmyk->c = c - k;
    cmyk->m = m - k;
    cmyk->y = y - k;
    cmyk->k = k;
}

void GfxCalRGBColorSpace::getDeviceN(const GfxColor &color, GfxColor *deviceN) const
{
    GfxCMYK cmyk;
    clearGfxColor(deviceN);
    getCMYK(color, &cmyk);
    deviceN->c[0] = cmyk.c;
    deviceN->c[1] = cmyk.m;
    deviceN->c[2] = cmyk.y;
    deviceN->c[3] = cmyk.k;
}

void GfxCalRGBColorSpace::getDefaultColor(GfxColor *color) const
{
    color->c[0] = 0;
    color->c[1] = 0;
    color->c[2] = 0;
}

//------------------------------------------------------------------------
// GfxDeviceCMYKColorSpace
//------------------------------------------------------------------------

GfxDeviceCMYKColorSpace::GfxDeviceCMYKColorSpace() = default;

GfxDeviceCMYKColorSpace::~GfxDeviceCMYKColorSpace() = default;

std::unique_ptr<GfxColorSpace> GfxDeviceCMYKColorSpace::copy() const
{
    return std::make_unique<GfxDeviceCMYKColorSpace>();
}

void GfxDeviceCMYKColorSpace::getGray(const GfxColor &color, GfxGray *gray) const
{
    *gray = clip01(static_cast<GfxColorComp>(gfxColorComp1 - color.c[3] - 0.3 * color.c[0] - 0.59 * color.c[1] - 0.11 * color.c[2] + 0.5));
}

void GfxDeviceCMYKColorSpace::getRGB(const GfxColor &color, GfxRGB *rgb) const
{
    double c, m, y, k, c1, m1, y1, k1, r, g, b;

    c = colToDbl(color.c[0]);
    m = colToDbl(color.c[1]);
    y = colToDbl(color.c[2]);
    k = colToDbl(color.c[3]);
    c1 = 1 - c;
    m1 = 1 - m;
    y1 = 1 - y;
    k1 = 1 - k;
    cmykToRGBMatrixMultiplication(c, m, y, k, c1, m1, y1, k1, r, g, b);
    rgb->r = clip01(dblToCol(r));
    rgb->g = clip01(dblToCol(g));
    rgb->b = clip01(dblToCol(b));
}

static inline void GfxDeviceCMYKColorSpacegetRGBLineHelper(unsigned char *&in, double &r, double &g, double &b)
{
    double c, m, y, k, c1, m1, y1, k1;

    c = byteToDbl(*in++);
    m = byteToDbl(*in++);
    y = byteToDbl(*in++);
    k = byteToDbl(*in++);
    c1 = 1 - c;
    m1 = 1 - m;
    y1 = 1 - y;
    k1 = 1 - k;
    cmykToRGBMatrixMultiplication(c, m, y, k, c1, m1, y1, k1, r, g, b);
}

void GfxDeviceCMYKColorSpace::getRGBLine(unsigned char *in, unsigned int *out, int length)
{
    double r, g, b;
    for (int i = 0; i < length; i++) {
        GfxDeviceCMYKColorSpacegetRGBLineHelper(in, r, g, b);
        *out++ = (dblToByte(clip01(r)) << 16) | (dblToByte(clip01(g)) << 8) | dblToByte(clip01(b));
    }
}

void GfxDeviceCMYKColorSpace::getRGBLine(unsigned char *in, unsigned char *out, int length)
{
    double r, g, b;

    for (int i = 0; i < length; i++) {
        GfxDeviceCMYKColorSpacegetRGBLineHelper(in, r, g, b);
        *out++ = dblToByte(clip01(r));
        *out++ = dblToByte(clip01(g));
        *out++ = dblToByte(clip01(b));
    }
}

void GfxDeviceCMYKColorSpace::getRGBXLine(unsigned char *in, unsigned char *out, int length)
{
    double r, g, b;

    for (int i = 0; i < length; i++) {
        GfxDeviceCMYKColorSpacegetRGBLineHelper(in, r, g, b);
        *out++ = dblToByte(clip01(r));
        *out++ = dblToByte(clip01(g));
        *out++ = dblToByte(clip01(b));
        *out++ = 255;
    }
}

void GfxDeviceCMYKColorSpace::getCMYKLine(unsigned char *in, unsigned char *out, int length)
{
    for (int i = 0; i < length; i++) {
        *out++ = *in++;
        *out++ = *in++;
        *out++ = *in++;
        *out++ = *in++;
    }
}

void GfxDeviceCMYKColorSpace::getDeviceNLine(unsigned char *in, unsigned char *out, int length)
{
    for (int i = 0; i < length; i++) {
        for (int j = 0; j < SPOT_NCOMPS + 4; j++) {
            out[j] = 0;
        }
        out[0] = *in++;
        out[1] = *in++;
        out[2] = *in++;
        out[3] = *in++;
        out += (SPOT_NCOMPS + 4);
    }
}

void GfxDeviceCMYKColorSpace::getCMYK(const GfxColor &color, GfxCMYK *cmyk) const
{
    cmyk->c = clip01(color.c[0]);
    cmyk->m = clip01(color.c[1]);
    cmyk->y = clip01(color.c[2]);
    cmyk->k = clip01(color.c[3]);
}

void GfxDeviceCMYKColorSpace::getDeviceN(const GfxColor &color, GfxColor *deviceN) const
{
    clearGfxColor(deviceN);
    deviceN->c[0] = clip01(color.c[0]);
    deviceN->c[1] = clip01(color.c[1]);
    deviceN->c[2] = clip01(color.c[2]);
    deviceN->c[3] = clip01(color.c[3]);
}

void GfxDeviceCMYKColorSpace::getDefaultColor(GfxColor *color) const
{
    color->c[0] = 0;
    color->c[1] = 0;
    color->c[2] = 0;
    color->c[3] = gfxColorComp1;
}

//------------------------------------------------------------------------
// GfxLabColorSpace
//------------------------------------------------------------------------

GfxLabColorSpace::GfxLabColorSpace()
{
    whiteX = whiteY = whiteZ = 1;
    blackX = blackY = blackZ = 0;
    aMin = bMin = -100;
    aMax = bMax = 100;
}

GfxLabColorSpace::~GfxLabColorSpace() = default;

std::unique_ptr<GfxColorSpace> GfxLabColorSpace::copy() const
{
    auto cs = std::make_unique<GfxLabColorSpace>();
    cs->whiteX = whiteX;
    cs->whiteY = whiteY;
    cs->whiteZ = whiteZ;
    cs->blackX = blackX;
    cs->blackY = blackY;
    cs->blackZ = blackZ;
    cs->aMin = aMin;
    cs->aMax = aMax;
    cs->bMin = bMin;
    cs->bMax = bMax;
#if USE_CMS
    cs->transform = transform;
#endif
    return cs;
}

std::unique_ptr<GfxColorSpace> GfxLabColorSpace::parse(const Array &arr, GfxState *state)
{
    Object obj1, obj2;

    obj1 = arr.get(1);
    if (!obj1.isDict()) {
        error(errSyntaxWarning, -1, "Bad Lab color space");
        return {};
    }
    auto cs = std::make_unique<GfxLabColorSpace>();
    bool ok = true;
    obj2 = obj1.dictLookup("WhitePoint");
    if (obj2.isArrayOfLength(3)) {
        cs->whiteX = obj2.arrayGet(0).getNum(&ok);
        cs->whiteY = obj2.arrayGet(1).getNum(&ok);
        cs->whiteZ = obj2.arrayGet(2).getNum(&ok);
    }
    obj2 = obj1.dictLookup("BlackPoint");
    if (obj2.isArrayOfLength(3)) {
        cs->blackX = obj2.arrayGet(0).getNum(&ok);
        cs->blackY = obj2.arrayGet(1).getNum(&ok);
        cs->blackZ = obj2.arrayGet(2).getNum(&ok);
    }
    obj2 = obj1.dictLookup("Range");
    if (obj2.isArrayOfLength(4)) {
        cs->aMin = obj2.arrayGet(0).getNum(&ok);
        cs->aMax = obj2.arrayGet(1).getNum(&ok);
        cs->bMin = obj2.arrayGet(2).getNum(&ok);
        cs->bMax = obj2.arrayGet(3).getNum(&ok);
    }

    if (!ok) {
        error(errSyntaxWarning, -1, "Bad Lab color space");
#if USE_CMS
        cs->transform = nullptr;
#endif
        return {};
    }

#if USE_CMS
    cs->transform = (state != nullptr) ? state->getXYZ2DisplayTransform() : nullptr;
#else
    (void)state;
#endif
    return cs;
}

void GfxLabColorSpace::getGray(const GfxColor &color, GfxGray *gray) const
{
    GfxRGB rgb;

#if USE_CMS
    if (transform != nullptr && transform->getTransformPixelType() == PT_GRAY) {
        unsigned char out[gfxColorMaxComps];
        double in[gfxColorMaxComps];

        getXYZ(color, &in[0], &in[1], &in[2]);
        bradford_transform_to_d50(in[0], in[1], in[2], whiteX, whiteY, whiteZ);
        transform->doTransform(in, out, 1);
        *gray = byteToCol(out[0]);
        return;
    }
#endif
    getRGB(color, &rgb);
    *gray = clip01(static_cast<GfxColorComp>(0.299 * rgb.r + 0.587 * rgb.g + 0.114 * rgb.b + 0.5));
}

// convert L*a*b* to media XYZ color space
// (not multiply by the white point)
void GfxLabColorSpace::getXYZ(const GfxColor &color, double *pX, double *pY, double *pZ)
{
    double X, Y, Z;
    double t1, t2;

    t1 = (colToDbl(color.c[0]) + 16) / 116;
    t2 = t1 + colToDbl(color.c[1]) / 500;
    if (t2 >= (6.0 / 29.0)) {
        X = t2 * t2 * t2;
    } else {
        X = (108.0 / 841.0) * (t2 - (4.0 / 29.0));
    }
    if (t1 >= (6.0 / 29.0)) {
        Y = t1 * t1 * t1;
    } else {
        Y = (108.0 / 841.0) * (t1 - (4.0 / 29.0));
    }
    t2 = t1 - colToDbl(color.c[2]) / 200;
    if (t2 >= (6.0 / 29.0)) {
        Z = t2 * t2 * t2;
    } else {
        Z = (108.0 / 841.0) * (t2 - (4.0 / 29.0));
    }
    *pX = X;
    *pY = Y;
    *pZ = Z;
}

void GfxLabColorSpace::getRGB(const GfxColor &color, GfxRGB *rgb) const
{
    double X, Y, Z;

    getXYZ(color, &X, &Y, &Z);
    X *= whiteX;
    Y *= whiteY;
    Z *= whiteZ;
#if USE_CMS
    if (transform != nullptr && transform->getTransformPixelType() == PT_RGB) {
        unsigned char out[gfxColorMaxComps];
        double in[gfxColorMaxComps];

        bradford_transform_to_d50(X, Y, Z, whiteX, whiteY, whiteZ);
        in[0] = X;
        in[1] = Y;
        in[2] = Z;
        transform->doTransform(in, out, 1);
        rgb->r = byteToCol(out[0]);
        rgb->g = byteToCol(out[1]);
        rgb->b = byteToCol(out[2]);
        return;
    }
    if (transform != nullptr && transform->getTransformPixelType() == PT_CMYK) {
        unsigned char out[gfxColorMaxComps];
        double in[gfxColorMaxComps];
        double c, m, y, k, c1, m1, y1, k1, r, g, b;

        bradford_transform_to_d50(X, Y, Z, whiteX, whiteY, whiteZ);
        in[0] = X;
        in[1] = Y;
        in[2] = Z;
        transform->doTransform(in, out, 1);
        c = byteToDbl(out[0]);
        m = byteToDbl(out[1]);
        y = byteToDbl(out[2]);
        k = byteToDbl(out[3]);
        c1 = 1 - c;
        m1 = 1 - m;
        y1 = 1 - y;
        k1 = 1 - k;
        cmykToRGBMatrixMultiplication(c, m, y, k, c1, m1, y1, k1, r, g, b);
        rgb->r = clip01(dblToCol(r));
        rgb->g = clip01(dblToCol(g));
        rgb->b = clip01(dblToCol(b));
        return;
    }
#endif
    bradford_transform_to_d65(X, Y, Z, whiteX, whiteY, whiteZ);
    // convert XYZ to RGB, including gamut mapping and gamma correction
    const double r = xyzrgb[0][0] * X + xyzrgb[0][1] * Y + xyzrgb[0][2] * Z;
    const double g = xyzrgb[1][0] * X + xyzrgb[1][1] * Y + xyzrgb[1][2] * Z;
    const double b = xyzrgb[2][0] * X + xyzrgb[2][1] * Y + xyzrgb[2][2] * Z;
    rgb->r = dblToCol(srgb_gamma_function(clip01(r)));
    rgb->g = dblToCol(srgb_gamma_function(clip01(g)));
    rgb->b = dblToCol(srgb_gamma_function(clip01(b)));
}

void GfxLabColorSpace::getCMYK(const GfxColor &color, GfxCMYK *cmyk) const
{
    GfxRGB rgb;
    GfxColorComp c, m, y, k;

#if USE_CMS
    if (transform != nullptr && transform->getTransformPixelType() == PT_CMYK) {
        double in[gfxColorMaxComps];
        unsigned char out[gfxColorMaxComps];

        getXYZ(color, &in[0], &in[1], &in[2]);
        bradford_transform_to_d50(in[0], in[1], in[2], whiteX, whiteY, whiteZ);
        transform->doTransform(in, out, 1);
        cmyk->c = byteToCol(out[0]);
        cmyk->m = byteToCol(out[1]);
        cmyk->y = byteToCol(out[2]);
        cmyk->k = byteToCol(out[3]);
        return;
    }
#endif
    getRGB(color, &rgb);
    c = clip01(gfxColorComp1 - rgb.r);
    m = clip01(gfxColorComp1 - rgb.g);
    y = clip01(gfxColorComp1 - rgb.b);
    k = c;
    if (m < k) {
        k = m;
    }
    if (y < k) {
        k = y;
    }
    cmyk->c = c - k;
    cmyk->m = m - k;
    cmyk->y = y - k;
    cmyk->k = k;
}

void GfxLabColorSpace::getDeviceN(const GfxColor &color, GfxColor *deviceN) const
{
    GfxCMYK cmyk;
    clearGfxColor(deviceN);
    getCMYK(color, &cmyk);
    deviceN->c[0] = cmyk.c;
    deviceN->c[1] = cmyk.m;
    deviceN->c[2] = cmyk.y;
    deviceN->c[3] = cmyk.k;
}

void GfxLabColorSpace::getDefaultColor(GfxColor *color) const
{
    color->c[0] = 0;
    if (aMin > 0) {
        color->c[1] = dblToCol(aMin);
    } else if (aMax < 0) {
        color->c[1] = dblToCol(aMax);
    } else {
        color->c[1] = 0;
    }
    if (bMin > 0) {
        color->c[2] = dblToCol(bMin);
    } else if (bMax < 0) {
        color->c[2] = dblToCol(bMax);
    } else {
        color->c[2] = 0;
    }
}

void GfxLabColorSpace::getDefaultRanges(double *decodeLow, double *decodeRange, int /*maxImgPixel*/) const
{
    decodeLow[0] = 0;
    decodeRange[0] = 100;
    decodeLow[1] = aMin;
    decodeRange[1] = aMax - aMin;
    decodeLow[2] = bMin;
    decodeRange[2] = bMax - bMin;
}

//------------------------------------------------------------------------
// GfxICCBasedColorSpace
//------------------------------------------------------------------------

GfxICCBasedColorSpace::GfxICCBasedColorSpace(int nCompsA, std::unique_ptr<GfxColorSpace> &&altA, const Ref *iccProfileStreamA) : alt(std::move(altA))
{
    nComps = nCompsA;
    iccProfileStream = *iccProfileStreamA;
    rangeMin[0] = rangeMin[1] = rangeMin[2] = rangeMin[3] = 0;
    rangeMax[0] = rangeMax[1] = rangeMax[2] = rangeMax[3] = 1;
#if USE_CMS
    transform = nullptr;
    lineTransform = nullptr;
    psCSA = nullptr;
#endif
}

GfxICCBasedColorSpace::~GfxICCBasedColorSpace()
{
#if USE_CMS
    if (psCSA) {
        gfree(psCSA);
    }
#endif
}

std::unique_ptr<GfxColorSpace> GfxICCBasedColorSpace::copy() const
{
    return copyAsOwnType();
}

std::unique_ptr<GfxICCBasedColorSpace> GfxICCBasedColorSpace::copyAsOwnType() const
{
    int i;

    auto cs = std::make_unique<GfxICCBasedColorSpace>(nComps, alt->copy(), &iccProfileStream);
    for (i = 0; i < 4; ++i) {
        cs->rangeMin[i] = rangeMin[i];
        cs->rangeMax[i] = rangeMax[i];
    }
#if USE_CMS
    cs->profile = profile;
    cs->transform = transform;
    cs->lineTransform = lineTransform;
#endif
    return cs;
}

std::unique_ptr<GfxColorSpace> GfxICCBasedColorSpace::parse(const Array &arr, OutputDev *out, GfxState *state, int recursion)
{
    int nCompsA;
    Dict *dict;
    Object obj1, obj2;
    int i;

    if (arr.getLength() < 2) {
        error(errSyntaxError, -1, "Bad ICCBased color space");
        return {};
    }
    const Object &obj1Ref = arr.getNF(1);
    const Ref iccProfileStreamA = obj1Ref.isRef() ? obj1Ref.getRef() : Ref::INVALID();
#if USE_CMS
    // check cache
    if (out && iccProfileStreamA != Ref::INVALID()) {
        if (auto *item = out->getIccColorSpaceCache()->lookup(iccProfileStreamA)) {
            std::unique_ptr<GfxICCBasedColorSpace> cs = item->copyAsOwnType();
            int transformIntent = cs->getIntent();
            int cmsIntent = INTENT_RELATIVE_COLORIMETRIC;
            if (state != nullptr) {
                cmsIntent = state->getCmsRenderingIntent();
            }
            if (transformIntent == cmsIntent) {
                return cs;
            }
        }
    }
#endif
    obj1 = arr.get(1);
    if (!obj1.isStream()) {
        error(errSyntaxWarning, -1, "Bad ICCBased color space (stream)");
        return nullptr;
    }
    dict = obj1.getStream()->getDict();
    obj2 = dict->lookup("N");
    if (!obj2.isInt()) {
        error(errSyntaxWarning, -1, "Bad ICCBased color space (N)");
        return nullptr;
    }
    nCompsA = obj2.getInt();
    if (nCompsA > 4) {
        error(errSyntaxError, -1, "ICCBased color space with too many ({0:d} > 4) components", nCompsA);
        nCompsA = 4;
    }
    obj2 = dict->lookup("Alternate");
    std::unique_ptr<GfxColorSpace> altA;
    if (obj2.isNull() || !(altA = GfxColorSpace::parse(nullptr, &obj2, out, state, recursion + 1))) {
        switch (nCompsA) {
        case 1:
            altA = std::make_unique<GfxDeviceGrayColorSpace>();
            break;
        case 3:
            altA = std::make_unique<GfxDeviceRGBColorSpace>();
            break;
        case 4:
            altA = std::make_unique<GfxDeviceCMYKColorSpace>();
            break;
        default:
            error(errSyntaxWarning, -1, "Bad ICCBased color space - invalid N");
            return nullptr;
        }
    }
    if (altA->getNComps() != nCompsA) {
        error(errSyntaxWarning, -1, "Bad ICCBased color space - N doesn't match alt color space");
        return {};
    }
    auto cs = std::make_unique<GfxICCBasedColorSpace>(nCompsA, std::move(altA), &iccProfileStreamA);
    obj2 = dict->lookup("Range");
    if (obj2.isArrayOfLength(2 * nCompsA)) {
        for (i = 0; i < nCompsA; ++i) {
            cs->rangeMin[i] = obj2.arrayGet(2 * i).getNumWithDefaultValue(0);
            cs->rangeMax[i] = obj2.arrayGet(2 * i + 1).getNumWithDefaultValue(1);
        }
    }

#if USE_CMS
    obj1 = arr.get(1);
    if (!obj1.isStream()) {
        error(errSyntaxWarning, -1, "Bad ICCBased color space (stream)");
        return {};
    }
    Stream *iccStream = obj1.getStream();

    const std::vector<unsigned char> profBuf = iccStream->toUnsignedChars(65536, 65536);
    auto hp = make_GfxLCMSProfilePtr(cmsOpenProfileFromMem(profBuf.data(), profBuf.size()));
    cs->profile = hp;
    if (!hp) {
        error(errSyntaxWarning, -1, "read ICCBased color space profile error");
    } else {
        cs->buildTransforms(state);
    }
    // put this colorSpace into cache
    if (out && iccProfileStreamA != Ref::INVALID()) {
        out->getIccColorSpaceCache()->put(iccProfileStreamA, cs->copyAsOwnType());
    }
#endif
    return cs;
}

#if USE_CMS
void GfxICCBasedColorSpace::buildTransforms(GfxState *state)
{
    auto dhp = (state != nullptr && state->getDisplayProfile() != nullptr) ? state->getDisplayProfile() : nullptr;
    if (!dhp) {
        dhp = GfxState::sRGBProfile;
    }
    unsigned int cst = getCMSColorSpaceType(cmsGetColorSpace(profile.get()));
    unsigned int dNChannels = getCMSNChannels(cmsGetColorSpace(dhp.get()));
    unsigned int dcst = getCMSColorSpaceType(cmsGetColorSpace(dhp.get()));
    cmsHTRANSFORM transformA;

    int cmsIntent = INTENT_RELATIVE_COLORIMETRIC;
    if (state != nullptr) {
        cmsIntent = state->getCmsRenderingIntent();
    }
    if ((transformA = cmsCreateTransform(profile.get(), COLORSPACE_SH(cst) | CHANNELS_SH(nComps) | BYTES_SH(1), dhp.get(), COLORSPACE_SH(dcst) | CHANNELS_SH(dNChannels) | BYTES_SH(1), cmsIntent, LCMS_FLAGS)) == nullptr) {
        error(errSyntaxWarning, -1, "Can't create transform");
        transform = nullptr;
    } else {
        transform = std::make_shared<GfxColorTransform>(transformA, cmsIntent, cst, dcst);
    }
    if (dcst == PT_RGB || dcst == PT_CMYK) {
        // create line transform only when the display is RGB type color space
        if ((transformA = cmsCreateTransform(profile.get(), CHANNELS_SH(nComps) | BYTES_SH(1), dhp.get(), (dcst == PT_RGB) ? TYPE_RGB_8 : TYPE_CMYK_8, cmsIntent, LCMS_FLAGS)) == nullptr) {
            error(errSyntaxWarning, -1, "Can't create transform");
            lineTransform = nullptr;
        } else {
            lineTransform = std::make_shared<GfxColorTransform>(transformA, cmsIntent, cst, dcst);
        }
    }
}
#endif

void GfxICCBasedColorSpace::getGray(const GfxColor &color, GfxGray *gray) const
{
#if USE_CMS
    if (transform != nullptr && transform->getTransformPixelType() == PT_GRAY) {
        unsigned char in[gfxColorMaxComps];
        unsigned char out[gfxColorMaxComps];

        if (nComps == 3 && transform->getInputPixelType() == PT_Lab) {
            in[0] = colToByte(dblToCol(colToDbl(color.c[0]) / 100.0));
            in[1] = colToByte(dblToCol((colToDbl(color.c[1]) + 128.0) / 255.0));
            in[2] = colToByte(dblToCol((colToDbl(color.c[2]) + 128.0) / 255.0));
        } else {
            for (int i = 0; i < nComps; i++) {
                in[i] = colToByte(color.c[i]);
            }
        }
        if (nComps <= 4) {
            unsigned int key = 0;
            for (int j = 0; j < nComps; j++) {
                key = (key << 8) + in[j];
            }
            auto it = cmsCache.find(key);
            if (it != cmsCache.end()) {
                unsigned int value = it->second;
                *gray = byteToCol(value & 0xff);
                return;
            }
        }
        transform->doTransform(in, out, 1);
        *gray = byteToCol(out[0]);
        if (nComps <= 4 && cmsCache.size() <= CMSCACHE_LIMIT) {
            unsigned int key = 0;
            for (int j = 0; j < nComps; j++) {
                key = (key << 8) + in[j];
            }
            unsigned int value = out[0];
            cmsCache.insert(std::pair<unsigned int, unsigned int>(key, value));
        }
    } else {
        GfxRGB rgb;
        getRGB(color, &rgb);
        *gray = clip01(static_cast<GfxColorComp>(0.3 * rgb.r + 0.59 * rgb.g + 0.11 * rgb.b + 0.5));
    }
#else
    alt->getGray(color, gray);
#endif
}

void GfxICCBasedColorSpace::getRGB(const GfxColor &color, GfxRGB *rgb) const
{
#if USE_CMS
    if (transform != nullptr && transform->getTransformPixelType() == PT_RGB) {
        unsigned char in[gfxColorMaxComps];
        unsigned char out[gfxColorMaxComps];

        if (nComps == 3 && transform->getInputPixelType() == PT_Lab) {
            in[0] = colToByte(dblToCol(colToDbl(color.c[0]) / 100.0));
            in[1] = colToByte(dblToCol((colToDbl(color.c[1]) + 128.0) / 255.0));
            in[2] = colToByte(dblToCol((colToDbl(color.c[2]) + 128.0) / 255.0));
        } else {
            for (int i = 0; i < nComps; i++) {
                in[i] = colToByte(color.c[i]);
            }
        }
        if (nComps <= 4) {
            unsigned int key = 0;
            for (int j = 0; j < nComps; j++) {
                key = (key << 8) + in[j];
            }
            auto it = cmsCache.find(key);
            if (it != cmsCache.end()) {
                unsigned int value = it->second;
                rgb->r = byteToCol(value >> 16);
                rgb->g = byteToCol((value >> 8) & 0xff);
                rgb->b = byteToCol(value & 0xff);
                return;
            }
        }
        transform->doTransform(in, out, 1);
        rgb->r = byteToCol(out[0]);
        rgb->g = byteToCol(out[1]);
        rgb->b = byteToCol(out[2]);
        if (nComps <= 4 && cmsCache.size() <= CMSCACHE_LIMIT) {
            unsigned int key = 0;
            for (int j = 0; j < nComps; j++) {
                key = (key << 8) + in[j];
            }
            unsigned int value = (out[0] << 16) + (out[1] << 8) + out[2];
            cmsCache.insert(std::pair<unsigned int, unsigned int>(key, value));
        }
    } else if (transform != nullptr && transform->getTransformPixelType() == PT_CMYK) {
        unsigned char in[gfxColorMaxComps];
        unsigned char out[gfxColorMaxComps];
        double c, m, y, k, c1, m1, y1, k1, r, g, b;

        if (nComps == 3 && transform->getInputPixelType() == PT_Lab) {
            in[0] = colToByte(dblToCol(colToDbl(color.c[0]) / 100.0));
            in[1] = colToByte(dblToCol((colToDbl(color.c[1]) + 128.0) / 255.0));
            in[2] = colToByte(dblToCol((colToDbl(color.c[2]) + 128.0) / 255.0));
        } else {
            for (int i = 0; i < nComps; i++) {
                in[i] = colToByte(color.c[i]);
            }
        }
        if (nComps <= 4) {
            unsigned int key = 0;
            for (int j = 0; j < nComps; j++) {
                key = (key << 8) + in[j];
            }
            auto it = cmsCache.find(key);
            if (it != cmsCache.end()) {
                unsigned int value = it->second;
                rgb->r = byteToCol(value >> 16);
                rgb->g = byteToCol((value >> 8) & 0xff);
                rgb->b = byteToCol(value & 0xff);
                return;
            }
        }
        transform->doTransform(in, out, 1);
        c = byteToDbl(out[0]);
        m = byteToDbl(out[1]);
        y = byteToDbl(out[2]);
        k = byteToDbl(out[3]);
        c1 = 1 - c;
        m1 = 1 - m;
        y1 = 1 - y;
        k1 = 1 - k;
        cmykToRGBMatrixMultiplication(c, m, y, k, c1, m1, y1, k1, r, g, b);
        rgb->r = clip01(dblToCol(r));
        rgb->g = clip01(dblToCol(g));
        rgb->b = clip01(dblToCol(b));
        if (nComps <= 4 && cmsCache.size() <= CMSCACHE_LIMIT) {
            unsigned int key = 0;
            for (int j = 0; j < nComps; j++) {
                key = (key << 8) + in[j];
            }
            unsigned int value = (dblToByte(r) << 16) + (dblToByte(g) << 8) + dblToByte(b);
            cmsCache.insert(std::pair<unsigned int, unsigned int>(key, value));
        }
    } else {
        alt->getRGB(color, rgb);
    }
#else
    alt->getRGB(color, rgb);
#endif
}

void GfxICCBasedColorSpace::getRGBLine(unsigned char *in, unsigned int *out, int length)
{
#if USE_CMS
    if (lineTransform != nullptr && lineTransform->getTransformPixelType() == PT_RGB) {
        auto *tmp = static_cast<unsigned char *>(gmallocn(3 * length, sizeof(unsigned char)));
        lineTransform->doTransform(in, tmp, length);
        for (int i = 0; i < length; ++i) {
            unsigned char *current = tmp + (i * 3);
            out[i] = (current[0] << 16) | (current[1] << 8) | current[2];
        }
        gfree(tmp);
    } else {
        alt->getRGBLine(in, out, length);
    }
#else
    alt->getRGBLine(in, out, length);
#endif
}

void GfxICCBasedColorSpace::getRGBLine(unsigned char *in, unsigned char *out, int length)
{
#if USE_CMS
    if (lineTransform != nullptr && lineTransform->getTransformPixelType() == PT_RGB) {
        auto *tmp = static_cast<unsigned char *>(gmallocn(3 * length, sizeof(unsigned char)));
        lineTransform->doTransform(in, tmp, length);
        unsigned char *current = tmp;
        for (int i = 0; i < length; ++i) {
            *out++ = *current++;
            *out++ = *current++;
            *out++ = *current++;
        }
        gfree(tmp);
    } else if (lineTransform != nullptr && lineTransform->getTransformPixelType() == PT_CMYK) {
        auto *tmp = static_cast<unsigned char *>(gmallocn(4 * length, sizeof(unsigned char)));
        lineTransform->doTransform(in, tmp, length);
        unsigned char *current = tmp;
        double c, m, y, k, c1, m1, y1, k1, r, g, b;
        for (int i = 0; i < length; ++i) {
            c = byteToDbl(*current++);
            m = byteToDbl(*current++);
            y = byteToDbl(*current++);
            k = byteToDbl(*current++);
            c1 = 1 - c;
            m1 = 1 - m;
            y1 = 1 - y;
            k1 = 1 - k;
            cmykToRGBMatrixMultiplication(c, m, y, k, c1, m1, y1, k1, r, g, b);
            *out++ = dblToByte(r);
            *out++ = dblToByte(g);
            *out++ = dblToByte(b);
        }
        gfree(tmp);
    } else {
        alt->getRGBLine(in, out, length);
    }
#else
    alt->getRGBLine(in, out, length);
#endif
}

void GfxICCBasedColorSpace::getRGBXLine(unsigned char *in, unsigned char *out, int length)
{
#if USE_CMS
    if (lineTransform != nullptr && lineTransform->getTransformPixelType() == PT_RGB) {
        auto *tmp = static_cast<unsigned char *>(gmallocn(3 * length, sizeof(unsigned char)));
        lineTransform->doTransform(in, tmp, length);
        unsigned char *current = tmp;
        for (int i = 0; i < length; ++i) {
            *out++ = *current++;
            *out++ = *current++;
            *out++ = *current++;
            *out++ = 255;
        }
        gfree(tmp);
    } else {
        alt->getRGBXLine(in, out, length);
    }
#else
    alt->getRGBXLine(in, out, length);
#endif
}

void GfxICCBasedColorSpace::getCMYKLine(unsigned char *in, unsigned char *out, int length)
{
#if USE_CMS
    if (lineTransform != nullptr && lineTransform->getTransformPixelType() == PT_CMYK) {
        transform->doTransform(in, out, length);
    } else if (lineTransform != nullptr && nComps != 4) {
        GfxColorComp c, m, y, k;
        auto *tmp = static_cast<unsigned char *>(gmallocn(3 * length, sizeof(unsigned char)));
        getRGBLine(in, tmp, length);
        unsigned char *p = tmp;
        for (int i = 0; i < length; i++) {
            c = byteToCol(255 - *p++);
            m = byteToCol(255 - *p++);
            y = byteToCol(255 - *p++);
            k = c;
            if (m < k) {
                k = m;
            }
            if (y < k) {
                k = y;
            }
            *out++ = colToByte(c - k);
            *out++ = colToByte(m - k);
            *out++ = colToByte(y - k);
            *out++ = colToByte(k);
        }
        gfree(tmp);
    } else {
        alt->getCMYKLine(in, out, length);
    }
#else
    alt->getCMYKLine(in, out, length);
#endif
}

void GfxICCBasedColorSpace::getDeviceNLine(unsigned char *in, unsigned char *out, int length)
{
#if USE_CMS
    if (lineTransform != nullptr && lineTransform->getTransformPixelType() == PT_CMYK) {
        auto *tmp = static_cast<unsigned char *>(gmallocn(4 * length, sizeof(unsigned char)));
        transform->doTransform(in, tmp, length);
        unsigned char *p = tmp;
        for (int i = 0; i < length; i++) {
            for (int j = 0; j < 4; j++) {
                *out++ = *p++;
            }
            for (int j = 4; j < SPOT_NCOMPS + 4; j++) {
                *out++ = 0;
            }
        }
        gfree(tmp);
    } else if (lineTransform != nullptr && nComps != 4) {
        GfxColorComp c, m, y, k;
        auto *tmp = static_cast<unsigned char *>(gmallocn(3 * length, sizeof(unsigned char)));
        getRGBLine(in, tmp, length);
        unsigned char *p = tmp;
        for (int i = 0; i < length; i++) {
            for (int j = 0; j < SPOT_NCOMPS + 4; j++) {
                out[j] = 0;
            }
            c = byteToCol(255 - *p++);
            m = byteToCol(255 - *p++);
            y = byteToCol(255 - *p++);
            k = c;
            if (m < k) {
                k = m;
            }
            if (y < k) {
                k = y;
            }
            out[0] = colToByte(c - k);
            out[1] = colToByte(m - k);
            out[2] = colToByte(y - k);
            out[3] = colToByte(k);
            out += (SPOT_NCOMPS + 4);
        }
        gfree(tmp);
    } else {
        alt->getDeviceNLine(in, out, length);
    }
#else
    alt->getDeviceNLine(in, out, length);
#endif
}

void GfxICCBasedColorSpace::getCMYK(const GfxColor &color, GfxCMYK *cmyk) const
{
#if USE_CMS
    if (transform != nullptr && transform->getTransformPixelType() == PT_CMYK) {
        unsigned char in[gfxColorMaxComps];
        unsigned char out[gfxColorMaxComps];

        if (nComps == 3 && transform->getInputPixelType() == PT_Lab) {
            in[0] = colToByte(dblToCol(colToDbl(color.c[0]) / 100.0));
            in[1] = colToByte(dblToCol((colToDbl(color.c[1]) + 128.0) / 255.0));
            in[2] = colToByte(dblToCol((colToDbl(color.c[2]) + 128.0) / 255.0));
        } else {
            for (int i = 0; i < nComps; i++) {
                in[i] = colToByte(color.c[i]);
            }
        }
        if (nComps <= 4) {
            unsigned int key = 0;
            for (int j = 0; j < nComps; j++) {
                key = (key << 8) + in[j];
            }
            auto it = cmsCache.find(key);
            if (it != cmsCache.end()) {
                unsigned int value = it->second;
                cmyk->c = byteToCol(value >> 24);
                cmyk->m = byteToCol((value >> 16) & 0xff);
                cmyk->y = byteToCol((value >> 8) & 0xff);
                cmyk->k = byteToCol(value & 0xff);
                return;
            }
        }
        transform->doTransform(in, out, 1);
        cmyk->c = byteToCol(out[0]);
        cmyk->m = byteToCol(out[1]);
        cmyk->y = byteToCol(out[2]);
        cmyk->k = byteToCol(out[3]);
        if (nComps <= 4 && cmsCache.size() <= CMSCACHE_LIMIT) {
            unsigned int key = 0;
            for (int j = 0; j < nComps; j++) {
                key = (key << 8) + in[j];
            }
            unsigned int value = (out[0] << 24) + (out[1] << 16) + (out[2] << 8) + out[3];
            cmsCache.insert(std::pair<unsigned int, unsigned int>(key, value));
        }
    } else if (nComps != 4 && transform != nullptr && transform->getTransformPixelType() == PT_RGB) {
        GfxRGB rgb;
        GfxColorComp c, m, y, k;

        getRGB(color, &rgb);
        c = clip01(gfxColorComp1 - rgb.r);
        m = clip01(gfxColorComp1 - rgb.g);
        y = clip01(gfxColorComp1 - rgb.b);
        k = c;
        if (m < k) {
            k = m;
        }
        if (y < k) {
            k = y;
        }
        cmyk->c = c - k;
        cmyk->m = m - k;
        cmyk->y = y - k;
        cmyk->k = k;
    } else {
        alt->getCMYK(color, cmyk);
    }
#else
    alt->getCMYK(color, cmyk);
#endif
}

bool GfxICCBasedColorSpace::useGetRGBLine() const
{
#if USE_CMS
    return lineTransform != nullptr || alt->useGetRGBLine();
#else
    return alt->useGetRGBLine();
#endif
}

bool GfxICCBasedColorSpace::useGetCMYKLine() const
{
#if USE_CMS
    return lineTransform != nullptr || alt->useGetCMYKLine();
#else
    return alt->useGetCMYKLine();
#endif
}

bool GfxICCBasedColorSpace::useGetDeviceNLine() const
{
#if USE_CMS
    return lineTransform != nullptr || alt->useGetDeviceNLine();
#else
    return alt->useGetDeviceNLine();
#endif
}

void GfxICCBasedColorSpace::getDeviceN(const GfxColor &color, GfxColor *deviceN) const
{
    GfxCMYK cmyk;
    clearGfxColor(deviceN);
    getCMYK(color, &cmyk);
    deviceN->c[0] = cmyk.c;
    deviceN->c[1] = cmyk.m;
    deviceN->c[2] = cmyk.y;
    deviceN->c[3] = cmyk.k;
}

void GfxICCBasedColorSpace::getDefaultColor(GfxColor *color) const
{
    int i;

    for (i = 0; i < nComps; ++i) {
        if (rangeMin[i] > 0) {
            color->c[i] = dblToCol(rangeMin[i]);
        } else if (rangeMax[i] < 0) {
            color->c[i] = dblToCol(rangeMax[i]);
        } else {
            color->c[i] = 0;
        }
    }
}

void GfxICCBasedColorSpace::getDefaultRanges(double *decodeLow, double *decodeRange, int maxImgPixel) const
{
    alt->getDefaultRanges(decodeLow, decodeRange, maxImgPixel);

#if 0
  // this is nominally correct, but some PDF files don't set the
  // correct ranges in the ICCBased dict
  int i;

  for (i = 0; i < nComps; ++i) {
    decodeLow[i] = rangeMin[i];
    decodeRange[i] = rangeMax[i] - rangeMin[i];
  }
#endif
}

#if USE_CMS
char *GfxICCBasedColorSpace::getPostScriptCSA()
{
    if (psCSA) {
        return psCSA;
    }

    if (!profile) {
        error(errSyntaxWarning, -1, "profile is nullptr");
        return nullptr;
    }

    void *rawprofile = profile.get();
    const int size = cmsGetPostScriptCSA(cmsGetProfileContextID(rawprofile), rawprofile, getIntent(), 0, nullptr, 0);
    if (size == 0) {
        error(errSyntaxWarning, -1, "PostScript CSA is nullptr");
        return nullptr;
    }

    psCSA = static_cast<char *>(gmalloc(size + 1));
    cmsGetPostScriptCSA(cmsGetProfileContextID(rawprofile), rawprofile, getIntent(), 0, psCSA, size);
    psCSA[size] = 0;

    return psCSA;
}
#endif

//------------------------------------------------------------------------
// GfxIndexedColorSpace
//------------------------------------------------------------------------

GfxIndexedColorSpace::GfxIndexedColorSpace(std::unique_ptr<GfxColorSpace> &&baseA, int indexHighA) : base(std::move(baseA))
{
    indexHigh = indexHighA;
    lookup = static_cast<unsigned char *>(gmallocn((indexHigh + 1) * base->getNComps(), sizeof(unsigned char)));
    overprintMask = base->getOverprintMask();
}

GfxIndexedColorSpace::~GfxIndexedColorSpace()
{
    gfree(lookup);
}

std::unique_ptr<GfxColorSpace> GfxIndexedColorSpace::copy() const
{

    auto cs = std::make_unique<GfxIndexedColorSpace>(base->copy(), indexHigh);
    memcpy(cs->lookup, lookup, (indexHigh + 1) * base->getNComps() * sizeof(unsigned char));
    return cs;
}

std::unique_ptr<GfxColorSpace> GfxIndexedColorSpace::parse(GfxResources *res, const Array &arr, OutputDev *out, GfxState *state, int recursion)
{
    std::unique_ptr<GfxColorSpace> baseA;
    Object obj1;

    if (arr.getLength() != 4) {
        error(errSyntaxWarning, -1, "Bad Indexed color space");
        return nullptr;
    }
    obj1 = arr.get(1);
    if (!(baseA = GfxColorSpace::parse(res, &obj1, out, state, recursion + 1))) {
        error(errSyntaxWarning, -1, "Bad Indexed color space (base color space)");
        return {};
    }
    obj1 = arr.get(2);
    if (!obj1.isInt()) {
        error(errSyntaxWarning, -1, "Bad Indexed color space (hival)");
        return {};
    }
    int indexHighA = obj1.getInt();
    if (indexHighA < 0 || indexHighA > 255) {
        // the PDF spec requires indexHigh to be in [0,255] -- allowing
        // values larger than 255 creates a security hole: if nComps *
        // indexHigh is greater than 2^31, the loop below may overwrite
        // past the end of the array
        int previousValue = indexHighA;
        if (indexHighA < 0) {
            indexHighA = 0;
        } else {
            indexHighA = 255;
        }
        error(errSyntaxWarning, -1, "Bad Indexed color space (invalid indexHigh value, was {0:d} using {1:d} to try to recover)", previousValue, indexHighA);
    }
    auto cs = std::make_unique<GfxIndexedColorSpace>(std::move(baseA), indexHighA);
    obj1 = arr.get(3);
    const int n = cs->getBase()->getNComps();
    if (obj1.isStream()) {
        Stream *stream = obj1.getStream();
        if (!stream->rewind()) {
            error(errSyntaxWarning, -1, "Bad Indexed color space (stream rewind failed)");
            return {};
        }
        for (int i = 0; i <= indexHighA; ++i) {
            const int readChars = stream->doGetChars(n, &cs->lookup[i * n]);
            for (int j = readChars; j < n; ++j) {
                error(errSyntaxWarning, -1, "Bad Indexed color space (lookup table stream too short) padding with zeroes");
                cs->lookup[i * n + j] = 0;
            }
        }
        stream->close();
    } else if (obj1.isString()) {
        if (obj1.getString().size() < static_cast<size_t>(indexHighA + 1) * n) {
            error(errSyntaxWarning, -1, "Bad Indexed color space (lookup table string too short)");
            return {};
        }
        const char *s = obj1.getString().c_str();
        for (int i = 0; i <= indexHighA; ++i) {
            for (int j = 0; j < n; ++j) {
                cs->lookup[i * n + j] = static_cast<unsigned char>(*s++);
            }
        }
    } else {
        error(errSyntaxWarning, -1, "Bad Indexed color space (lookup table)");
        return {};
    }
    return cs;
}

GfxColor *GfxIndexedColorSpace::mapColorToBase(const GfxColor &color, GfxColor *baseColor) const
{
    unsigned char *p;
    double low[gfxColorMaxComps], range[gfxColorMaxComps];
    int n, i;

    n = base->getNComps();
    base->getDefaultRanges(low, range, indexHigh);
    const int idx = static_cast<int>(colToDbl(color.c[0]) + 0.5) * n;
    if (likely((idx + n - 1 < (indexHigh + 1) * base->getNComps()) && idx >= 0)) {
        p = &lookup[idx];
        for (i = 0; i < n; ++i) {
            baseColor->c[i] = dblToCol(low[i] + (p[i] / 255.0) * range[i]);
        }
    } else {
        for (i = 0; i < n; ++i) {
            baseColor->c[i] = 0;
        }
    }
    return baseColor;
}

void GfxIndexedColorSpace::getGray(const GfxColor &color, GfxGray *gray) const
{
    GfxColor color2;

    base->getGray(*mapColorToBase(color, &color2), gray);
}

void GfxIndexedColorSpace::getRGB(const GfxColor &color, GfxRGB *rgb) const
{
    GfxColor color2;

    base->getRGB(*mapColorToBase(color, &color2), rgb);
}

void GfxIndexedColorSpace::getRGBLine(unsigned char *in, unsigned int *out, int length)
{
    unsigned char *line;
    int i, j, n;

    n = base->getNComps();
    line = static_cast<unsigned char *>(gmallocn(length, n));
    for (i = 0; i < length; i++) {
        for (j = 0; j < n; j++) {
            line[i * n + j] = lookup[in[i] * n + j];
        }
    }

    base->getRGBLine(line, out, length);

    gfree(line);
}

void GfxIndexedColorSpace::getRGBLine(unsigned char *in, unsigned char *out, int length)
{
    unsigned char *line;
    int i, j, n;

    n = base->getNComps();
    line = static_cast<unsigned char *>(gmallocn(length, n));
    for (i = 0; i < length; i++) {
        for (j = 0; j < n; j++) {
            line[i * n + j] = lookup[in[i] * n + j];
        }
    }

    base->getRGBLine(line, out, length);

    gfree(line);
}

void GfxIndexedColorSpace::getRGBXLine(unsigned char *in, unsigned char *out, int length)
{
    unsigned char *line;
    int i, j, n;

    n = base->getNComps();
    line = static_cast<unsigned char *>(gmallocn(length, n));
    for (i = 0; i < length; i++) {
        for (j = 0; j < n; j++) {
            line[i * n + j] = lookup[in[i] * n + j];
        }
    }

    base->getRGBXLine(line, out, length);

    gfree(line);
}

void GfxIndexedColorSpace::getCMYKLine(unsigned char *in, unsigned char *out, int length)
{
    unsigned char *line;
    int i, j, n;

    n = base->getNComps();
    line = static_cast<unsigned char *>(gmallocn(length, n));
    for (i = 0; i < length; i++) {
        for (j = 0; j < n; j++) {
            line[i * n + j] = lookup[in[i] * n + j];
        }
    }

    base->getCMYKLine(line, out, length);

    gfree(line);
}

void GfxIndexedColorSpace::getDeviceNLine(unsigned char *in, unsigned char *out, int length)
{
    unsigned char *line;
    int i, j, n;

    n = base->getNComps();
    line = static_cast<unsigned char *>(gmallocn(length, n));
    for (i = 0; i < length; i++) {
        for (j = 0; j < n; j++) {
            line[i * n + j] = lookup[in[i] * n + j];
        }
    }

    base->getDeviceNLine(line, out, length);

    gfree(line);
}

void GfxIndexedColorSpace::getCMYK(const GfxColor &color, GfxCMYK *cmyk) const
{
    GfxColor color2;

    base->getCMYK(*mapColorToBase(color, &color2), cmyk);
}

void GfxIndexedColorSpace::getDeviceN(const GfxColor &color, GfxColor *deviceN) const
{
    GfxColor color2;

    base->getDeviceN(*mapColorToBase(color, &color2), deviceN);
}

void GfxIndexedColorSpace::getDefaultColor(GfxColor *color) const
{
    color->c[0] = 0;
}

void GfxIndexedColorSpace::getDefaultRanges(double *decodeLow, double *decodeRange, int maxImgPixel) const
{
    decodeLow[0] = 0;
    decodeRange[0] = maxImgPixel;
}

//------------------------------------------------------------------------
// GfxSeparationColorSpace
//------------------------------------------------------------------------

GfxSeparationColorSpace::GfxSeparationColorSpace(std::unique_ptr<GooString> &&nameA, std::unique_ptr<GfxColorSpace> &&altA, std::unique_ptr<Function> funcA) : name(std::move(nameA)), alt(std::move(altA))
{
    func = std::move(funcA);
    nonMarking = !name->compare("None");
    if (!name->compare("Cyan")) {
        overprintMask = 0x01;
    } else if (!name->compare("Magenta")) {
        overprintMask = 0x02;
    } else if (!name->compare("Yellow")) {
        overprintMask = 0x04;
    } else if (!name->compare("Black")) {
        overprintMask = 0x08;
    } else if (!name->compare("All")) {
        overprintMask = 0xffffffff;
    }
}

GfxSeparationColorSpace::GfxSeparationColorSpace(std::unique_ptr<GooString> &&nameA, std::unique_ptr<GfxColorSpace> &&altA, std::unique_ptr<Function> funcA, bool nonMarkingA, unsigned int overprintMaskA, const std::vector<int> &mappingA,
                                                 PrivateTag /*unused*/)
    : name(std::move(nameA)), alt(std::move(altA))
{
    func = std::move(funcA);
    nonMarking = nonMarkingA;
    overprintMask = overprintMaskA;
    mapping = mappingA;
}

GfxSeparationColorSpace::~GfxSeparationColorSpace() = default;

std::unique_ptr<GfxColorSpace> GfxSeparationColorSpace::copy() const
{
    return copyAsOwnType();
}

std::unique_ptr<GfxSeparationColorSpace> GfxSeparationColorSpace::copyAsOwnType() const
{
    return std::make_unique<GfxSeparationColorSpace>(name->copy(), alt->copy(), func->copy(), nonMarking, overprintMask, mapping);
}

//~ handle the 'All' and 'None' colorants
std::unique_ptr<GfxColorSpace> GfxSeparationColorSpace::parse(GfxResources *res, const Array &arr, OutputDev *out, GfxState *state, int recursion)
{
    std::unique_ptr<GfxColorSpace> altA;
    std::unique_ptr<Function> funcA;
    Object obj1;

    if (arr.getLength() != 4) {
        error(errSyntaxWarning, -1, "Bad Separation color space");
        return {};
    }
    obj1 = arr.get(1);
    if (!obj1.isName()) {
        error(errSyntaxWarning, -1, "Bad Separation color space (name)");
        return {};
    }
    std::unique_ptr<GooString> nameA = std::make_unique<GooString>(obj1.getNameString());
    obj1 = arr.get(2);
    if (!(altA = GfxColorSpace::parse(res, &obj1, out, state, recursion + 1))) {
        error(errSyntaxWarning, -1, "Bad Separation color space (alternate color space)");
        return {};
    }
    obj1 = arr.get(3);
    if (!(funcA = Function::parse(&obj1))) {
        return {};
    }
    if (funcA->getInputSize() != 1) {
        error(errSyntaxWarning, -1, "Bad SeparationColorSpace function");
        return {};
    }
    if (altA->getNComps() > funcA->getOutputSize()) {
        return {};
    }
    return std::make_unique<GfxSeparationColorSpace>(std::move(nameA), std::move(altA), std::move(funcA));
}

void GfxSeparationColorSpace::getGray(const GfxColor &color, GfxGray *gray) const
{
    double x;
    double c[gfxColorMaxComps];
    GfxColor color2;
    int i;

    if (alt->getMode() == csDeviceGray && name->compare("Black") == 0) {
        *gray = clip01(gfxColorComp1 - color.c[0]);
    } else {
        x = colToDbl(color.c[0]);
        func->transform(&x, c);
        for (i = 0; i < alt->getNComps(); ++i) {
            color2.c[i] = dblToCol(c[i]);
        }
        alt->getGray(color2, gray);
    }
}

void GfxSeparationColorSpace::getRGB(const GfxColor &color, GfxRGB *rgb) const
{
    double x;
    double c[gfxColorMaxComps];
    GfxColor color2;
    int i;

    if (alt->getMode() == csDeviceGray && name->compare("Black") == 0) {
        rgb->r = clip01(gfxColorComp1 - color.c[0]);
        rgb->g = clip01(gfxColorComp1 - color.c[0]);
        rgb->b = clip01(gfxColorComp1 - color.c[0]);
    } else {
        x = colToDbl(color.c[0]);
        func->transform(&x, c);
        const int altNComps = alt->getNComps();
        for (i = 0; i < altNComps; ++i) {
            color2.c[i] = dblToCol(c[i]);
        }
        alt->getRGB(color2, rgb);
    }
}

void GfxSeparationColorSpace::getCMYK(const GfxColor &color, GfxCMYK *cmyk) const
{
    double x;
    double c[gfxColorMaxComps];
    GfxColor color2;
    int i;

    if (name->compare("Black") == 0) {
        cmyk->c = 0;
        cmyk->m = 0;
        cmyk->y = 0;
        cmyk->k = color.c[0];
    } else if (name->compare("Cyan") == 0) {
        cmyk->c = color.c[0];
        cmyk->m = 0;
        cmyk->y = 0;
        cmyk->k = 0;
    } else if (name->compare("Magenta") == 0) {
        cmyk->c = 0;
        cmyk->m = color.c[0];
        cmyk->y = 0;
        cmyk->k = 0;
    } else if (name->compare("Yellow") == 0) {
        cmyk->c = 0;
        cmyk->m = 0;
        cmyk->y = color.c[0];
        cmyk->k = 0;
    } else {
        x = colToDbl(color.c[0]);
        func->transform(&x, c);
        for (i = 0; i < alt->getNComps(); ++i) {
            color2.c[i] = dblToCol(c[i]);
        }
        alt->getCMYK(color2, cmyk);
    }
}

void GfxSeparationColorSpace::getDeviceN(const GfxColor &color, GfxColor *deviceN) const
{
    clearGfxColor(deviceN);
    if (mapping.empty() || mapping[0] == -1) {
        GfxCMYK cmyk;

        getCMYK(color, &cmyk);
        deviceN->c[0] = cmyk.c;
        deviceN->c[1] = cmyk.m;
        deviceN->c[2] = cmyk.y;
        deviceN->c[3] = cmyk.k;
    } else {
        deviceN->c[mapping[0]] = color.c[0];
    }
}

void GfxSeparationColorSpace::getDefaultColor(GfxColor *color) const
{
    color->c[0] = gfxColorComp1;
}

void GfxSeparationColorSpace::createMapping(std::vector<std::unique_ptr<GfxSeparationColorSpace>> *separationList, size_t maxSepComps)
{
    if (nonMarking) {
        return;
    }
    mapping.resize(1);
    switch (overprintMask) {
    case 0x01:
        mapping[0] = 0;
        break;
    case 0x02:
        mapping[0] = 1;
        break;
    case 0x04:
        mapping[0] = 2;
        break;
    case 0x08:
        mapping[0] = 3;
        break;
    default:
        unsigned int newOverprintMask = 0x10;
        for (std::size_t i = 0; i < separationList->size(); i++) {
            const std::unique_ptr<GfxSeparationColorSpace> &sepCS = (*separationList)[i];
            if (!sepCS->getName()->compare(name->toStr())) {
                if (sepCS->getFunc()->hasDifferentResultSet(func.get())) {
                    error(errSyntaxWarning, -1, "Different functions found for '{0:t}', convert immediately", name.get());
                    mapping.clear();
                    return;
                }
                mapping[0] = i + 4;
                overprintMask = newOverprintMask;
                return;
            }
            newOverprintMask <<= 1;
        }
        if (separationList->size() == maxSepComps) {
            error(errSyntaxWarning, -1, "Too many ({0:ulld}) spots, convert '{1:t}' immediately", static_cast<unsigned long long>(maxSepComps), name.get());
            mapping.clear();
            return;
        }
        mapping[0] = separationList->size() + 4;
        separationList->push_back(copyAsOwnType());
        overprintMask = newOverprintMask;
        break;
    }
}

//------------------------------------------------------------------------
// GfxDeviceNColorSpace
//------------------------------------------------------------------------

GfxDeviceNColorSpace::GfxDeviceNColorSpace(int nCompsA, std::vector<std::string> &&namesA, std::unique_ptr<GfxColorSpace> &&altA, std::unique_ptr<Function> funcA, std::vector<std::unique_ptr<GfxSeparationColorSpace>> &&sepsCSA)
    : nComps(nCompsA), names(std::move(namesA)), alt(std::move(altA))
{
    func = std::move(funcA);
    sepsCS = std::move(sepsCSA);
    nonMarking = true;
    overprintMask = 0;
    for (int i = 0; i < nComps; ++i) {
        if (names[i] != "None") {
            nonMarking = false;
        }
        if (names[i] == "Cyan") {
            overprintMask |= 0x01;
        } else if (names[i] == "Magenta") {
            overprintMask |= 0x02;
        } else if (names[i] == "Yellow") {
            overprintMask |= 0x04;
        } else if (names[i] == "Black") {
            overprintMask |= 0x08;
        } else if (names[i] == "All") {
            overprintMask = 0xffffffff;
        } else if (names[i] != "None") {
            overprintMask = 0x0f;
        }
    }
}

GfxDeviceNColorSpace::GfxDeviceNColorSpace(int nCompsA, const std::vector<std::string> &namesA, std::unique_ptr<GfxColorSpace> &&altA, std::unique_ptr<Function> funcA, std::vector<std::unique_ptr<GfxSeparationColorSpace>> &&sepsCSA,
                                           const std::vector<int> &mappingA, bool nonMarkingA, unsigned int overprintMaskA, PrivateTag /*unused*/)
    : nComps(nCompsA), names(namesA), alt(std::move(altA))
{
    func = std::move(funcA);
    sepsCS = std::move(sepsCSA);
    mapping = mappingA;
    nonMarking = nonMarkingA;
    overprintMask = overprintMaskA;
}

GfxDeviceNColorSpace::~GfxDeviceNColorSpace() = default;

std::unique_ptr<GfxColorSpace> GfxDeviceNColorSpace::copy() const
{
    std::vector<std::unique_ptr<GfxSeparationColorSpace>> sepsCSA;
    sepsCSA.reserve(sepsCS.size());
    for (const std::unique_ptr<GfxSeparationColorSpace> &scs : sepsCS) {
        if (likely(scs != nullptr)) {
            sepsCSA.push_back(scs->copyAsOwnType());
        }
    }
    return std::make_unique<GfxDeviceNColorSpace>(nComps, names, alt->copy(), func->copy(), std::move(sepsCSA), mapping, nonMarking, overprintMask);
}

//~ handle the 'None' colorant
std::unique_ptr<GfxColorSpace> GfxDeviceNColorSpace::parse(GfxResources *res, const Array &arr, OutputDev *out, GfxState *state, int recursion)
{
    int nCompsA;
    std::vector<std::string> namesA;
    std::unique_ptr<GfxColorSpace> altA;
    std::unique_ptr<Function> funcA;
    Object obj1;
    std::vector<std::unique_ptr<GfxSeparationColorSpace>> separationList;

    if (arr.getLength() != 4 && arr.getLength() != 5) {
        error(errSyntaxWarning, -1, "Bad DeviceN color space");
        return nullptr;
    }
    obj1 = arr.get(1);
    if (!obj1.isArray()) {
        error(errSyntaxWarning, -1, "Bad DeviceN color space (names)");
        return nullptr;
    }
    nCompsA = obj1.arrayGetLength();
    if (nCompsA > gfxColorMaxComps) {
        error(errSyntaxWarning, -1, "DeviceN color space with too many ({0:d} > {1:d}) components", nCompsA, gfxColorMaxComps);
        nCompsA = gfxColorMaxComps;
    }
    for (int i = 0; i < nCompsA; ++i) {
        Object obj2 = obj1.arrayGet(i);
        if (!obj2.isName()) {
            error(errSyntaxWarning, -1, "Bad DeviceN color space (names)");
            nCompsA = i;
            return nullptr;
        }
        namesA.emplace_back(obj2.getNameString());
    }
    obj1 = arr.get(2);
    if (!(altA = GfxColorSpace::parse(res, &obj1, out, state, recursion + 1))) {
        error(errSyntaxWarning, -1, "Bad DeviceN color space (alternate color space)");
        return nullptr;
    }
    obj1 = arr.get(3);
    if (!(funcA = Function::parse(&obj1))) {
        return nullptr;
    }
    if (arr.getLength() == 5) {
        obj1 = arr.get(4);
        if (!obj1.isDict()) {
            error(errSyntaxWarning, -1, "Bad DeviceN color space (attributes)");
            return nullptr;
        }
        Dict *attribs = obj1.getDict();
        Object obj2 = attribs->lookup("Colorants");
        if (obj2.isDict()) {
            Dict *colorants = obj2.getDict();
            for (int i = 0; i < colorants->getLength(); i++) {
                Object obj3 = colorants->getVal(i);
                if (obj3.isArray()) {
                    auto cs = GfxSeparationColorSpace::parse(res, *obj3.getArray(), out, state, recursion);
                    if (cs) {
                        separationList.push_back(std::unique_ptr<GfxSeparationColorSpace>(static_cast<GfxSeparationColorSpace *>(cs.release())));
                    }
                } else {
                    error(errSyntaxWarning, -1, "Bad DeviceN color space (colorant value entry is not an Array)");
                    return nullptr;
                }
            }
        }
    }

    if (likely(nCompsA >= funcA->getInputSize() && altA->getNComps() <= funcA->getOutputSize())) {
        return std::make_unique<GfxDeviceNColorSpace>(nCompsA, std::move(namesA), std::move(altA), std::move(funcA), std::move(separationList));
    }
    return nullptr;
}

void GfxDeviceNColorSpace::getGray(const GfxColor &color, GfxGray *gray) const
{
    double x[gfxColorMaxComps], c[gfxColorMaxComps];
    GfxColor color2;
    int i;

    for (i = 0; i < nComps; ++i) {
        x[i] = colToDbl(color.c[i]);
    }
    func->transform(x, c);
    for (i = 0; i < alt->getNComps(); ++i) {
        color2.c[i] = dblToCol(c[i]);
    }
    alt->getGray(color2, gray);
}

void GfxDeviceNColorSpace::getRGB(const GfxColor &color, GfxRGB *rgb) const
{
    double x[gfxColorMaxComps], c[gfxColorMaxComps];
    GfxColor color2;
    int i;

    for (i = 0; i < nComps; ++i) {
        x[i] = colToDbl(color.c[i]);
    }
    func->transform(x, c);
    for (i = 0; i < alt->getNComps(); ++i) {
        color2.c[i] = dblToCol(c[i]);
    }
    alt->getRGB(color2, rgb);
}

void GfxDeviceNColorSpace::getCMYK(const GfxColor &color, GfxCMYK *cmyk) const
{
    double x[gfxColorMaxComps], c[gfxColorMaxComps];
    GfxColor color2;
    int i;

    for (i = 0; i < nComps; ++i) {
        x[i] = colToDbl(color.c[i]);
    }
    func->transform(x, c);
    for (i = 0; i < alt->getNComps(); ++i) {
        color2.c[i] = dblToCol(c[i]);
    }
    alt->getCMYK(color2, cmyk);
}

void GfxDeviceNColorSpace::getDeviceN(const GfxColor &color, GfxColor *deviceN) const
{
    clearGfxColor(deviceN);
    if (mapping.empty()) {
        GfxCMYK cmyk;

        getCMYK(color, &cmyk);
        deviceN->c[0] = cmyk.c;
        deviceN->c[1] = cmyk.m;
        deviceN->c[2] = cmyk.y;
        deviceN->c[3] = cmyk.k;
    } else {
        for (int j = 0; j < nComps; j++) {
            if (mapping[j] != -1) {
                deviceN->c[mapping[j]] = color.c[j];
            }
        }
    }
}

void GfxDeviceNColorSpace::getDefaultColor(GfxColor *color) const
{
    int i;

    for (i = 0; i < nComps; ++i) {
        color->c[i] = gfxColorComp1;
    }
}

void GfxDeviceNColorSpace::createMapping(std::vector<std::unique_ptr<GfxSeparationColorSpace>> *separationList, size_t maxSepComps)
{
    if (nonMarking) { // None
        return;
    }
    mapping.resize(nComps);
    unsigned int newOverprintMask = 0;
    for (int i = 0; i < nComps; i++) {
        const std::string &name = names[i];
        if (name == "None") {
            mapping[i] = -1;
        } else if (name == "Cyan") {
            newOverprintMask |= 0x01;
            mapping[i] = 0;
        } else if (name == "Magenta") {
            newOverprintMask |= 0x02;
            mapping[i] = 1;
        } else if (name == "Yellow") {
            newOverprintMask |= 0x04;
            mapping[i] = 2;
        } else if (name == "Black") {
            newOverprintMask |= 0x08;
            mapping[i] = 3;
        } else {
            unsigned int startOverprintMask = 0x10;
            bool found = false;
            const Function *sepFunc = nullptr;
            if (nComps == 1) {
                sepFunc = func.get();
            } else {
                for (const std::unique_ptr<GfxSeparationColorSpace> &sepCS : sepsCS) {
                    if (!sepCS->getName()->compare(name)) {
                        sepFunc = sepCS->getFunc();
                        break;
                    }
                }
            }
            for (std::size_t j = 0; j < separationList->size(); j++) {
                const std::unique_ptr<GfxSeparationColorSpace> &sepCS = (*separationList)[j];
                if (!sepCS->getName()->compare(name)) {
                    if (sepFunc != nullptr && sepCS->getFunc()->hasDifferentResultSet(sepFunc)) {
                        error(errSyntaxWarning, -1, "Different functions found for '{0:r}', convert immediately", &name);
                        mapping.clear();
                        overprintMask = 0xffffffff;
                        return;
                    }
                    mapping[i] = j + 4;
                    newOverprintMask |= startOverprintMask;
                    found = true;
                    break;
                }
                startOverprintMask <<= 1;
            }
            if (!found) {
                if (separationList->size() == maxSepComps) {
                    error(errSyntaxWarning, -1, "Too many ({0:ulld}) spots, convert '{1:r}' immediately", static_cast<unsigned long long>(maxSepComps), &name);
                    mapping.clear();
                    overprintMask = 0xffffffff;
                    return;
                }
                mapping[i] = separationList->size() + 4;
                newOverprintMask |= startOverprintMask;
                if (nComps == 1) {
                    separationList->push_back(std::make_unique<GfxSeparationColorSpace>(std::make_unique<GooString>(name), alt->copy(), func->copy()));
                } else {
                    for (const std::unique_ptr<GfxSeparationColorSpace> &sepCS : sepsCS) {
                        if (!sepCS->getName()->compare(name)) {
                            found = true;
                            separationList->push_back(sepCS->copyAsOwnType());
                            break;
                        }
                    }
                    if (!found) {
                        error(errSyntaxWarning, -1, "DeviceN has no suitable colorant");
                        mapping.clear();
                        overprintMask = 0xffffffff;
                        return;
                    }
                }
            }
        }
    }
    overprintMask = newOverprintMask;
}

//------------------------------------------------------------------------
// GfxPatternColorSpace
//------------------------------------------------------------------------

GfxPatternColorSpace::GfxPatternColorSpace(std::unique_ptr<GfxColorSpace> &&underA) : under(std::move(underA)) { }

GfxPatternColorSpace::~GfxPatternColorSpace() = default;

std::unique_ptr<GfxColorSpace> GfxPatternColorSpace::copy() const
{
    return std::make_unique<GfxPatternColorSpace>(under ? under->copy() : nullptr);
}

std::unique_ptr<GfxColorSpace> GfxPatternColorSpace::parse(GfxResources *res, const Array &arr, OutputDev *out, GfxState *state, int recursion)
{
    Object obj1;

    if (arr.getLength() != 1 && arr.getLength() != 2) {
        error(errSyntaxWarning, -1, "Bad Pattern color space");
        return {};
    }
    std::unique_ptr<GfxColorSpace> underA;
    if (arr.getLength() == 2) {
        obj1 = arr.get(1);
        if (!(underA = GfxColorSpace::parse(res, &obj1, out, state, recursion + 1))) {
            error(errSyntaxWarning, -1, "Bad Pattern color space (underlying color space)");
            return {};
        }
    }
    return std::make_unique<GfxPatternColorSpace>(std::move(underA));
}

void GfxPatternColorSpace::getGray(const GfxColor & /*color*/, GfxGray *gray) const
{
    *gray = 0;
}

void GfxPatternColorSpace::getRGB(const GfxColor & /*color*/, GfxRGB *rgb) const
{
    rgb->r = rgb->g = rgb->b = 0;
}

void GfxPatternColorSpace::getCMYK(const GfxColor & /*color*/, GfxCMYK *cmyk) const
{
    cmyk->c = cmyk->m = cmyk->y = 0;
    cmyk->k = 1;
}

void GfxPatternColorSpace::getDeviceN(const GfxColor & /*color*/, GfxColor *deviceN) const
{
    clearGfxColor(deviceN);
    deviceN->c[3] = 1;
}

void GfxPatternColorSpace::getDefaultColor(GfxColor *color) const
{
    color->c[0] = 0;
}

//------------------------------------------------------------------------
// Pattern
//------------------------------------------------------------------------

GfxPattern::GfxPattern(int typeA, int patternRefNumA) : type(typeA), patternRefNum(patternRefNumA) { }

GfxPattern::~GfxPattern() = default;

std::unique_ptr<GfxPattern> GfxPattern::parse(GfxResources *res, Object *obj, OutputDev *out, GfxState *state, int patternRefNum)
{
    Object obj1;

    if (obj->isDict()) {
        obj1 = obj->dictLookup("PatternType");
    } else if (obj->isStream()) {
        obj1 = obj->getStream()->getDict()->lookup("PatternType");
    } else {
        return {};
    }
    if (obj1.isInt() && obj1.getInt() == 1) {
        return GfxTilingPattern::parse(obj, patternRefNum);
    }
    if (obj1.isInt() && obj1.getInt() == 2) {
        return GfxShadingPattern::parse(res, obj, out, state, patternRefNum);
    }
    return {};
}

//------------------------------------------------------------------------
// GfxTilingPattern
//------------------------------------------------------------------------

std::unique_ptr<GfxTilingPattern> GfxTilingPattern::parse(Object *patObj, int patternRefNum)
{
    Dict *dict;
    int paintTypeA, tilingTypeA;
    double xStepA, yStepA;
    Object resDictA;
    Object obj1;
    int i;

    if (!patObj->isStream()) {
        return nullptr;
    }
    dict = patObj->getStream()->getDict();

    obj1 = dict->lookup("PaintType");
    if (obj1.isInt()) {
        paintTypeA = obj1.getInt();
    } else {
        paintTypeA = 1;
        error(errSyntaxWarning, -1, "Invalid or missing PaintType in pattern");
    }
    obj1 = dict->lookup("TilingType");
    if (obj1.isInt()) {
        tilingTypeA = obj1.getInt();
    } else {
        tilingTypeA = 1;
        error(errSyntaxWarning, -1, "Invalid or missing TilingType in pattern");
    }
    std::array<double, 4> bboxA;
    bboxA[0] = bboxA[1] = 0;
    bboxA[2] = bboxA[3] = 1;
    obj1 = dict->lookup("BBox");
    if (obj1.isArrayOfLength(4)) {
        for (i = 0; i < 4; ++i) {
            Object obj2 = obj1.arrayGet(i);
            if (obj2.isNum()) {
                bboxA[i] = obj2.getNum();
            }
        }
    } else {
        error(errSyntaxWarning, -1, "Invalid or missing BBox in pattern");
    }
    obj1 = dict->lookup("XStep");
    if (obj1.isNum()) {
        xStepA = obj1.getNum();
    } else {
        xStepA = 1;
        error(errSyntaxWarning, -1, "Invalid or missing XStep in pattern");
    }
    obj1 = dict->lookup("YStep");
    if (obj1.isNum()) {
        yStepA = obj1.getNum();
    } else {
        yStepA = 1;
        error(errSyntaxWarning, -1, "Invalid or missing YStep in pattern");
    }
    resDictA = dict->lookup("Resources");
    if (!resDictA.isDict()) {
        error(errSyntaxWarning, -1, "Invalid or missing Resources in pattern");
    }
    std::array<double, 6> matrixA;
    matrixA[0] = 1;
    matrixA[1] = 0;
    matrixA[2] = 0;
    matrixA[3] = 1;
    matrixA[4] = 0;
    matrixA[5] = 0;
    obj1 = dict->lookup("Matrix");
    if (obj1.isArrayOfLength(6)) {
        for (i = 0; i < 6; ++i) {
            Object obj2 = obj1.arrayGet(i);
            if (obj2.isNum()) {
                matrixA[i] = obj2.getNum();
            }
        }
    }

    auto *pattern = new GfxTilingPattern(paintTypeA, tilingTypeA, bboxA, xStepA, yStepA, &resDictA, matrixA, patObj, patternRefNum);
    return std::unique_ptr<GfxTilingPattern>(pattern);
}

GfxTilingPattern::GfxTilingPattern(int paintTypeA, int tilingTypeA, const std::array<double, 4> &bboxA, double xStepA, double yStepA, const Object *resDictA, const std::array<double, 6> &matrixA, const Object *contentStreamA,
                                   int patternRefNumA)
    : GfxPattern(1, patternRefNumA), bbox(bboxA), matrix(matrixA)
{
    paintType = paintTypeA;
    tilingType = tilingTypeA;
    xStep = xStepA;
    yStep = yStepA;
    resDict = resDictA->copy();
    contentStream = contentStreamA->copy();
}

GfxTilingPattern::~GfxTilingPattern() = default;

std::unique_ptr<GfxPattern> GfxTilingPattern::copy() const
{
    auto *pattern = new GfxTilingPattern(paintType, tilingType, bbox, xStep, yStep, &resDict, matrix, &contentStream, getPatternRefNum());
    return std::unique_ptr<GfxTilingPattern>(pattern);
}

//------------------------------------------------------------------------
// GfxShadingPattern
//------------------------------------------------------------------------

std::unique_ptr<GfxShadingPattern> GfxShadingPattern::parse(GfxResources *res, Object *patObj, OutputDev *out, GfxState *state, int patternRefNum)
{
    Dict *dict;
    Object obj1;
    int i;

    if (!patObj->isDict()) {
        return {};
    }
    dict = patObj->getDict();

    obj1 = dict->lookup("Shading");
    std::unique_ptr<GfxShading> shadingA = GfxShading::parse(res, &obj1, out, state);
    if (!shadingA) {
        return {};
    }

    std::array<double, 6> matrixA;
    matrixA[0] = 1;
    matrixA[1] = 0;
    matrixA[2] = 0;
    matrixA[3] = 1;
    matrixA[4] = 0;
    matrixA[5] = 0;
    obj1 = dict->lookup("Matrix");
    if (obj1.isArrayOfLength(6)) {
        for (i = 0; i < 6; ++i) {
            Object obj2 = obj1.arrayGet(i);
            if (obj2.isNum()) {
                matrixA[i] = obj2.getNum();
            }
        }
    }

    auto *pattern = new GfxShadingPattern(std::move(shadingA), matrixA, patternRefNum);
    return std::unique_ptr<GfxShadingPattern>(pattern);
}

GfxShadingPattern::GfxShadingPattern(std::unique_ptr<GfxShading> &&shadingA, const std::array<double, 6> &matrixA, int patternRefNumA) : GfxPattern(2, patternRefNumA), shading(std::move(shadingA)), matrix(matrixA) { }

GfxShadingPattern::~GfxShadingPattern() = default;

std::unique_ptr<GfxPattern> GfxShadingPattern::copy() const
{
    auto *pattern = new GfxShadingPattern(shading->copy(), matrix, getPatternRefNum());
    return std::unique_ptr<GfxShadingPattern>(pattern);
}

//------------------------------------------------------------------------
// GfxShading
//------------------------------------------------------------------------

GfxShading::GfxShading(int typeA)
{
    type = static_cast<ShadingType>(typeA);
}

GfxShading::GfxShading(const GfxShading *shading)
{
    int i;

    type = shading->type;
    colorSpace = shading->colorSpace->copy();
    for (i = 0; i < gfxColorMaxComps; ++i) {
        background.c[i] = shading->background.c[i];
    }
    hasBackground = shading->hasBackground;
    bbox_xMin = shading->bbox_xMin;
    bbox_yMin = shading->bbox_yMin;
    bbox_xMax = shading->bbox_xMax;
    bbox_yMax = shading->bbox_yMax;
    hasBBox = shading->hasBBox;
}

GfxShading::~GfxShading() = default;

std::unique_ptr<GfxShading> GfxShading::parse(GfxResources *res, Object *obj, OutputDev *out, GfxState *state)
{
    Dict *dict;
    int typeA;
    Object obj1;

    if (obj->isDict()) {
        dict = obj->getDict();
    } else if (obj->isStream()) {
        dict = obj->getStream()->getDict();
    } else {
        return {};
    }

    obj1 = dict->lookup("ShadingType");
    if (!obj1.isInt()) {
        error(errSyntaxWarning, -1, "Invalid ShadingType in shading dictionary");
        return {};
    }
    typeA = obj1.getInt();

    switch (typeA) {
    case 1:
        return GfxFunctionShading::parse(res, dict, out, state);
        break;
    case 2:
        return GfxAxialShading::parse(res, dict, out, state);
        break;
    case 3:
        return GfxRadialShading::parse(res, dict, out, state);
        break;
    case 4:
        if (obj->isStream()) {
            return GfxGouraudTriangleShading::parse(res, 4, dict, obj->getStream(), out, state);
        } else {
            error(errSyntaxWarning, -1, "Invalid Type 4 shading object");
        }
        break;
    case 5:
        if (obj->isStream()) {
            return GfxGouraudTriangleShading::parse(res, 5, dict, obj->getStream(), out, state);
        } else {
            error(errSyntaxWarning, -1, "Invalid Type 5 shading object");
        }
        break;
    case 6:
        if (obj->isStream()) {
            return GfxPatchMeshShading::parse(res, 6, dict, obj->getStream(), out, state);
        } else {
            error(errSyntaxWarning, -1, "Invalid Type 6 shading object");
        }
        break;
    case 7:
        if (obj->isStream()) {
            return GfxPatchMeshShading::parse(res, 7, dict, obj->getStream(), out, state);
        } else {
            error(errSyntaxWarning, -1, "Invalid Type 7 shading object");
        }
        break;
    default:
        error(errSyntaxWarning, -1, "Unimplemented shading type {0:d}", typeA);
    }
    return {};
}

bool GfxShading::init(GfxResources *res, Dict *dict, OutputDev *out, GfxState *state)
{
    Object obj1;
    int i;

    obj1 = dict->lookup("ColorSpace");
    if (!(colorSpace = GfxColorSpace::parse(res, &obj1, out, state))) {
        error(errSyntaxWarning, -1, "Bad color space in shading dictionary");
        return false;
    }

    for (i = 0; i < gfxColorMaxComps; ++i) {
        background.c[i] = 0;
    }
    hasBackground = false;
    obj1 = dict->lookup("Background");
    if (obj1.isArray()) {
        if (obj1.arrayGetLength() == colorSpace->getNComps()) {
            hasBackground = true;
            for (i = 0; i < colorSpace->getNComps(); ++i) {
                Object obj2 = obj1.arrayGet(i);
                background.c[i] = dblToCol(obj2.getNum(&hasBackground));
            }
            if (!hasBackground) {
                error(errSyntaxWarning, -1, "Bad Background in shading dictionary");
            }
        } else {
            error(errSyntaxWarning, -1, "Bad Background in shading dictionary");
        }
    }

    bbox_xMin = bbox_yMin = bbox_xMax = bbox_yMax = 0;
    hasBBox = false;
    obj1 = dict->lookup("BBox");
    if (obj1.isArray()) {
        if (obj1.arrayGetLength() == 4) {
            hasBBox = true;
            bbox_xMin = obj1.arrayGet(0).getNum(&hasBBox);
            bbox_yMin = obj1.arrayGet(1).getNum(&hasBBox);
            bbox_xMax = obj1.arrayGet(2).getNum(&hasBBox);
            bbox_yMax = obj1.arrayGet(3).getNum(&hasBBox);
            if (!hasBBox) {
                error(errSyntaxWarning, -1, "Bad BBox in shading dictionary (Values not numbers)");
            }
        } else {
            error(errSyntaxWarning, -1, "Bad BBox in shading dictionary");
        }
    }

    return true;
}

//------------------------------------------------------------------------
// GfxFunctionShading
//------------------------------------------------------------------------

GfxFunctionShading::GfxFunctionShading(double x0A, double y0A, double x1A, double y1A, const std::array<double, 6> &matrixA, std::vector<std::unique_ptr<Function>> &&funcsA) : GfxShading(1), matrix(matrixA), funcs(std::move(funcsA))
{
    x0 = x0A;
    y0 = y0A;
    x1 = x1A;
    y1 = y1A;
}

GfxFunctionShading::GfxFunctionShading(const GfxFunctionShading *shading) : GfxShading(shading), matrix(shading->matrix)
{
    x0 = shading->x0;
    y0 = shading->y0;
    x1 = shading->x1;
    y1 = shading->y1;
    for (const auto &f : shading->funcs) {
        funcs.emplace_back(f->copy());
    }
}

GfxFunctionShading::~GfxFunctionShading() = default;

std::unique_ptr<GfxFunctionShading> GfxFunctionShading::parse(GfxResources *res, Dict *dict, OutputDev *out, GfxState *state)
{
    double x0A, y0A, x1A, y1A;
    std::vector<std::unique_ptr<Function>> funcsA;
    Object obj1;
    int i;

    x0A = y0A = 0;
    x1A = y1A = 1;
    obj1 = dict->lookup("Domain");
    if (obj1.isArrayOfLength(4)) {
        bool decodeOk = true;
        x0A = obj1.arrayGet(0).getNum(&decodeOk);
        x1A = obj1.arrayGet(1).getNum(&decodeOk);
        y0A = obj1.arrayGet(2).getNum(&decodeOk);
        y1A = obj1.arrayGet(3).getNum(&decodeOk);

        if (!decodeOk) {
            error(errSyntaxWarning, -1, "Invalid Domain array in function shading dictionary");
            return {};
        }
    }

    std::array<double, 6> matrixA;
    matrixA[0] = 1;
    matrixA[1] = 0;
    matrixA[2] = 0;
    matrixA[3] = 1;
    matrixA[4] = 0;
    matrixA[5] = 0;
    obj1 = dict->lookup("Matrix");
    if (obj1.isArrayOfLength(6)) {
        bool decodeOk = true;
        matrixA[0] = obj1.arrayGet(0).getNum(&decodeOk);
        matrixA[1] = obj1.arrayGet(1).getNum(&decodeOk);
        matrixA[2] = obj1.arrayGet(2).getNum(&decodeOk);
        matrixA[3] = obj1.arrayGet(3).getNum(&decodeOk);
        matrixA[4] = obj1.arrayGet(4).getNum(&decodeOk);
        matrixA[5] = obj1.arrayGet(5).getNum(&decodeOk);

        if (!decodeOk) {
            error(errSyntaxWarning, -1, "Invalid Matrix array in function shading dictionary");
            return {};
        }
    }

    obj1 = dict->lookup("Function");
    if (obj1.isArray()) {
        const int nFuncsA = obj1.arrayGetLength();
        if (nFuncsA > gfxColorMaxComps || nFuncsA <= 0) {
            error(errSyntaxWarning, -1, "Invalid Function array in shading dictionary");
            return {};
        }
        for (i = 0; i < nFuncsA; ++i) {
            Object obj2 = obj1.arrayGet(i);
            std::unique_ptr<Function> f = Function::parse(&obj2);
            if (!f) {
                return {};
            }
            funcsA.emplace_back(std::move(f));
        }
    } else {
        std::unique_ptr<Function> f = Function::parse(&obj1);
        if (!f) {
            return {};
        }
        funcsA.emplace_back(std::move(f));
    }

    auto shading = std::make_unique<GfxFunctionShading>(x0A, y0A, x1A, y1A, matrixA, std::move(funcsA));
    if (!shading->init(res, dict, out, state)) {
        return {};
    }
    return shading;
}

bool GfxFunctionShading::init(GfxResources *res, Dict *dict, OutputDev *out, GfxState *state)
{
    const bool parentInit = GfxShading::init(res, dict, out, state);
    if (!parentInit) {
        return false;
    }

    // funcs needs to be one of the two:
    //  * One function 2-in -> nComps-out
    //  * nComps functions 2-in -> 1-out
    const int nComps = colorSpace->getNComps();
    const int nFuncs = funcs.size();
    if (nFuncs == 1) {
        if (funcs[0]->getInputSize() != 2) {
            error(errSyntaxWarning, -1, "GfxFunctionShading: function with input size != 2");
            return false;
        }
        if (funcs[0]->getOutputSize() != nComps) {
            error(errSyntaxWarning, -1, "GfxFunctionShading: function with wrong output size");
            return false;
        }
    } else if (nFuncs == nComps) {
        for (const std::unique_ptr<Function> &f : funcs) {
            if (f->getInputSize() != 2) {
                error(errSyntaxWarning, -1, "GfxFunctionShading: function with input size != 2");
                return false;
            }
            if (f->getOutputSize() != 1) {
                error(errSyntaxWarning, -1, "GfxFunctionShading: function with wrong output size");
                return false;
            }
        }
    } else {
        return false;
    }

    return true;
}

std::unique_ptr<GfxShading> GfxFunctionShading::copy() const
{
    return std::make_unique<GfxFunctionShading>(this);
}

void GfxFunctionShading::getColor(double x, double y, GfxColor *color) const
{
    double in[2], out[gfxColorMaxComps];

    // NB: there can be one function with n outputs or n functions with
    // one output each (where n = number of color components)
    for (double &i : out) {
        i = 0;
    }
    in[0] = x;
    in[1] = y;
    for (int i = 0; i < getNFuncs(); ++i) {
        funcs[i]->transform(in, &out[i]);
    }
    for (int i = 0; i < gfxColorMaxComps; ++i) {
        color->c[i] = dblToCol(out[i]);
    }
}

//------------------------------------------------------------------------
// GfxUnivariateShading
//------------------------------------------------------------------------

GfxUnivariateShading::GfxUnivariateShading(int typeA, double t0A, double t1A, std::vector<std::unique_ptr<Function>> &&funcsA, bool extend0A, bool extend1A) : GfxShading(typeA), funcs(std::move(funcsA))
{
    t0 = t0A;
    t1 = t1A;
    extend0 = extend0A;
    extend1 = extend1A;

    cacheSize = 0;
    lastMatch = 0;
    cacheBounds = nullptr;
    cacheCoeff = nullptr;
    cacheValues = nullptr;
}

GfxUnivariateShading::GfxUnivariateShading(const GfxUnivariateShading *shading) : GfxShading(shading)
{
    t0 = shading->t0;
    t1 = shading->t1;
    for (const auto &f : shading->funcs) {
        funcs.emplace_back(f->copy());
    }
    extend0 = shading->extend0;
    extend1 = shading->extend1;

    cacheSize = 0;
    lastMatch = 0;
    cacheBounds = nullptr;
    cacheCoeff = nullptr;
    cacheValues = nullptr;
}

GfxUnivariateShading::~GfxUnivariateShading()
{
    gfree(cacheBounds);
}

int GfxUnivariateShading::getColor(double t, GfxColor *color)
{
    double out[gfxColorMaxComps];

    // NB: there can be one function with n outputs or n functions with
    // one output each (where n = number of color components)
    const int nComps = getNFuncs() * funcs[0]->getOutputSize();

    if (cacheSize > 0) {
        double x, ix, *l, *u, *upper;

        if (cacheBounds[lastMatch - 1] >= t) {
            upper = std::lower_bound(cacheBounds, cacheBounds + lastMatch - 1, t);
            lastMatch = static_cast<int>(upper - cacheBounds);
            lastMatch = std::min<int>(std::max<int>(1, lastMatch), cacheSize - 1);
        } else if (cacheBounds[lastMatch] < t) {
            upper = std::lower_bound(cacheBounds + lastMatch + 1, cacheBounds + cacheSize, t);
            lastMatch = static_cast<int>(upper - cacheBounds);
            lastMatch = std::min<int>(std::max<int>(1, lastMatch), cacheSize - 1);
        }

        x = (t - cacheBounds[lastMatch - 1]) * cacheCoeff[lastMatch];
        ix = 1.0 - x;
        u = cacheValues + lastMatch * nComps;
        l = u - nComps;

        for (int i = 0; i < nComps; ++i) {
            out[i] = ix * l[i] + x * u[i];
        }
    } else {
        for (int i = 0; i < nComps; ++i) {
            out[i] = 0;
        }
        for (int i = 0; i < getNFuncs(); ++i) {
            funcs[i]->transform(&t, &out[i]);
        }
    }

    for (int i = 0; i < nComps; ++i) {
        color->c[i] = dblToCol(out[i]);
    }
    return nComps;
}

void GfxUnivariateShading::setupCache(const Matrix *ctm, double xMin, double yMin, double xMax, double yMax)
{
    double sMin, sMax, tMin, tMax, upperBound;
    int i, j, nComps, maxSize;

    gfree(cacheBounds);
    cacheBounds = nullptr;
    cacheSize = 0;

    if (unlikely(getNFuncs() < 1)) {
        return;
    }

    // NB: there can be one function with n outputs or n functions with
    // one output each (where n = number of color components)
    nComps = getNFuncs() * funcs[0]->getOutputSize();

    getParameterRange(&sMin, &sMax, xMin, yMin, xMax, yMax);
    upperBound = ctm->norm() * getDistance(sMin, sMax);
    maxSize = static_cast<int>(ceil(upperBound));
    maxSize = std::max<int>(maxSize, 2);

    {
        double x[4], y[4];

        ctm->transform(xMin, yMin, &x[0], &y[0]);
        ctm->transform(xMax, yMin, &x[1], &y[1]);
        ctm->transform(xMin, yMax, &x[2], &y[2]);
        ctm->transform(xMax, yMax, &x[3], &y[3]);

        xMin = xMax = x[0];
        yMin = yMax = y[0];
        for (i = 1; i < 4; i++) {
            xMin = std::min<double>(xMin, x[i]);
            yMin = std::min<double>(yMin, y[i]);
            xMax = std::max<double>(xMax, x[i]);
            yMax = std::max<double>(yMax, y[i]);
        }
    }

    if (maxSize > (xMax - xMin) * (yMax - yMin)) {
        return;
    }

    if (t0 < t1) {
        tMin = t0 + sMin * (t1 - t0);
        tMax = t0 + sMax * (t1 - t0);
    } else {
        tMin = t0 + sMax * (t1 - t0);
        tMax = t0 + sMin * (t1 - t0);
    }

    cacheBounds = static_cast<double *>(gmallocn_checkoverflow(maxSize, sizeof(double) * (nComps + 2)));
    if (unlikely(!cacheBounds)) {
        return;
    }
    cacheCoeff = cacheBounds + maxSize;
    cacheValues = cacheCoeff + maxSize;

    if (cacheSize != 0) {
        for (j = 0; j < cacheSize; ++j) {
            cacheCoeff[j] = 1 / (cacheBounds[j + 1] - cacheBounds[j]);
        }
    } else if (tMax != tMin) {
        double step = (tMax - tMin) / (maxSize - 1);
        double coeff = (maxSize - 1) / (tMax - tMin);

        cacheSize = maxSize;

        for (j = 0; j < cacheSize; ++j) {
            cacheBounds[j] = tMin + j * step;
            cacheCoeff[j] = coeff;

            for (i = 0; i < nComps; ++i) {
                cacheValues[j * nComps + i] = 0;
            }
            for (i = 0; i < getNFuncs(); ++i) {
                funcs[i]->transform(&cacheBounds[j], &cacheValues[j * nComps + i]);
            }
        }
    }

    lastMatch = 1;
}

bool GfxUnivariateShading::init(GfxResources *res, Dict *dict, OutputDev *out, GfxState *state)
{
    const bool parentInit = GfxShading::init(res, dict, out, state);
    if (!parentInit) {
        return false;
    }

    // funcs needs to be one of the two:
    //  * One function 1-in -> nComps-out
    //  * nComps functions 1-in -> 1-out
    const int nComps = colorSpace->getNComps();
    const int nFuncs = funcs.size();
    if (nFuncs == 1) {
        if (funcs[0]->getInputSize() != 1) {
            error(errSyntaxWarning, -1, "GfxUnivariateShading: function with input size != 2");
            return false;
        }
        if (funcs[0]->getOutputSize() != nComps) {
            error(errSyntaxWarning, -1, "GfxUnivariateShading: function with wrong output size");
            return false;
        }
    } else if (nFuncs == nComps) {
        for (const std::unique_ptr<Function> &f : funcs) {
            if (f->getInputSize() != 1) {
                error(errSyntaxWarning, -1, "GfxUnivariateShading: function with input size != 2");
                return false;
            }
            if (f->getOutputSize() != 1) {
                error(errSyntaxWarning, -1, "GfxUnivariateShading: function with wrong output size");
                return false;
            }
        }
    } else {
        return false;
    }

    return true;
}

//------------------------------------------------------------------------
// GfxAxialShading
//------------------------------------------------------------------------

GfxAxialShading::GfxAxialShading(double x0A, double y0A, double x1A, double y1A, double t0A, double t1A, std::vector<std::unique_ptr<Function>> &&funcsA, bool extend0A, bool extend1A)
    : GfxUnivariateShading(2, t0A, t1A, std::move(funcsA), extend0A, extend1A)
{
    x0 = x0A;
    y0 = y0A;
    x1 = x1A;
    y1 = y1A;
}

GfxAxialShading::GfxAxialShading(const GfxAxialShading *shading) : GfxUnivariateShading(shading)
{
    x0 = shading->x0;
    y0 = shading->y0;
    x1 = shading->x1;
    y1 = shading->y1;
}

GfxAxialShading::~GfxAxialShading() = default;

std::unique_ptr<GfxAxialShading> GfxAxialShading::parse(GfxResources *res, Dict *dict, OutputDev *out, GfxState *state)
{
    double x0A, y0A, x1A, y1A;
    double t0A, t1A;
    std::vector<std::unique_ptr<Function>> funcsA;
    bool extend0A, extend1A;
    Object obj1;

    x0A = y0A = x1A = y1A = 0;
    obj1 = dict->lookup("Coords");
    if (obj1.isArrayOfLength(4)) {
        x0A = obj1.arrayGet(0).getNumWithDefaultValue(0);
        y0A = obj1.arrayGet(1).getNumWithDefaultValue(0);
        x1A = obj1.arrayGet(2).getNumWithDefaultValue(0);
        y1A = obj1.arrayGet(3).getNumWithDefaultValue(0);
    } else {
        error(errSyntaxWarning, -1, "Missing or invalid Coords in shading dictionary");
        return {};
    }

    t0A = 0;
    t1A = 1;
    obj1 = dict->lookup("Domain");
    if (obj1.isArrayOfLength(2)) {
        t0A = obj1.arrayGet(0).getNumWithDefaultValue(0);
        t1A = obj1.arrayGet(1).getNumWithDefaultValue(1);
    }

    obj1 = dict->lookup("Function");
    if (obj1.isArray()) {
        const int nFuncsA = obj1.arrayGetLength();
        if (nFuncsA > gfxColorMaxComps || nFuncsA == 0) {
            error(errSyntaxWarning, -1, "Invalid Function array in shading dictionary");
            return {};
        }
        for (int i = 0; i < nFuncsA; ++i) {
            Object obj2 = obj1.arrayGet(i);
            std::unique_ptr<Function> f = Function::parse(&obj2);
            if (!f) {
                return {};
            }
            funcsA.emplace_back(std::move(f));
        }
    } else {
        std::unique_ptr<Function> f = Function::parse(&obj1);
        if (!f) {
            return {};
        }
        funcsA.emplace_back(std::move(f));
    }

    extend0A = extend1A = false;
    obj1 = dict->lookup("Extend");
    if (obj1.isArrayOfLength(2)) {
        Object obj2 = obj1.arrayGet(0);
        if (obj2.isBool()) {
            extend0A = obj2.getBool();
        } else {
            error(errSyntaxWarning, -1, "Invalid axial shading extend (0)");
        }
        obj2 = obj1.arrayGet(1);
        if (obj2.isBool()) {
            extend1A = obj2.getBool();
        } else {
            error(errSyntaxWarning, -1, "Invalid axial shading extend (1)");
        }
    }

    auto shading = std::make_unique<GfxAxialShading>(x0A, y0A, x1A, y1A, t0A, t1A, std::move(funcsA), extend0A, extend1A);
    if (!shading->init(res, dict, out, state)) {
        return {};
    }
    return shading;
}

std::unique_ptr<GfxShading> GfxAxialShading::copy() const
{
    return std::make_unique<GfxAxialShading>(this);
}

double GfxAxialShading::getDistance(double sMin, double sMax) const
{
    double xMin, yMin, xMax, yMax;

    xMin = x0 + sMin * (x1 - x0);
    yMin = y0 + sMin * (y1 - y0);
    xMax = x0 + sMax * (x1 - x0);
    yMax = y0 + sMax * (y1 - y0);

    return hypot(xMax - xMin, yMax - yMin);
}

void GfxAxialShading::getParameterRange(double *lower, double *upper, double xMin, double yMin, double xMax, double yMax)
{
    double pdx, pdy, invsqnorm, tdx, tdy, t, range[2];

    // Linear gradients are orthogonal to the line passing through their
    // extremes. Because of convexity, the parameter range can be
    // computed as the convex hull (one the real line) of the parameter
    // values of the 4 corners of the box.
    //
    // The parameter value t for a point (x,y) can be computed as:
    //
    //   t = (p2 - p1) . (x,y) / |p2 - p1|^2
    //
    // t0  is the t value for the top left corner
    // tdx is the difference between left and right corners
    // tdy is the difference between top and bottom corners

    pdx = x1 - x0;
    pdy = y1 - y0;
    const double invsqnorm_denominator = (pdx * pdx + pdy * pdy);
    if (unlikely(invsqnorm_denominator == 0)) {
        *lower = 0;
        *upper = 0;
        return;
    }
    invsqnorm = 1.0 / invsqnorm_denominator;
    pdx *= invsqnorm;
    pdy *= invsqnorm;

    t = (xMin - x0) * pdx + (yMin - y0) * pdy;
    tdx = (xMax - xMin) * pdx;
    tdy = (yMax - yMin) * pdy;

    // Because of the linearity of the t value, tdx can simply be added
    // the t0 to move along the top edge. After this, *lower and *upper
    // represent the parameter range for the top edge, so extending it
    // to include the whole box simply requires adding tdy to the
    // correct extreme.

    range[0] = range[1] = t;
    if (tdx < 0) {
        range[0] += tdx;
    } else {
        range[1] += tdx;
    }

    if (tdy < 0) {
        range[0] += tdy;
    } else {
        range[1] += tdy;
    }

    *lower = std::max<double>(0., std::min<double>(1., range[0]));
    *upper = std::max<double>(0., std::min<double>(1., range[1]));
}

//------------------------------------------------------------------------
// GfxRadialShading
//------------------------------------------------------------------------

#ifndef RADIAL_EPSILON
#    define RADIAL_EPSILON (1. / 1024 / 1024)
#endif

GfxRadialShading::GfxRadialShading(double x0A, double y0A, double r0A, double x1A, double y1A, double r1A, double t0A, double t1A, std::vector<std::unique_ptr<Function>> &&funcsA, bool extend0A, bool extend1A)
    : GfxUnivariateShading(3, t0A, t1A, std::move(funcsA), extend0A, extend1A)
{
    x0 = x0A;
    y0 = y0A;
    r0 = r0A;
    x1 = x1A;
    y1 = y1A;
    r1 = r1A;
}

GfxRadialShading::GfxRadialShading(const GfxRadialShading *shading) : GfxUnivariateShading(shading)
{
    x0 = shading->x0;
    y0 = shading->y0;
    r0 = shading->r0;
    x1 = shading->x1;
    y1 = shading->y1;
    r1 = shading->r1;
}

GfxRadialShading::~GfxRadialShading() = default;

std::unique_ptr<GfxRadialShading> GfxRadialShading::parse(GfxResources *res, Dict *dict, OutputDev *out, GfxState *state)
{
    double x0A, y0A, r0A, x1A, y1A, r1A;
    double t0A, t1A;
    std::vector<std::unique_ptr<Function>> funcsA;
    bool extend0A, extend1A;
    Object obj1;
    int i;

    x0A = y0A = r0A = x1A = y1A = r1A = 0;
    obj1 = dict->lookup("Coords");
    if (obj1.isArrayOfLength(6)) {
        x0A = obj1.arrayGet(0).getNumWithDefaultValue(0);
        y0A = obj1.arrayGet(1).getNumWithDefaultValue(0);
        r0A = obj1.arrayGet(2).getNumWithDefaultValue(0);
        x1A = obj1.arrayGet(3).getNumWithDefaultValue(0);
        y1A = obj1.arrayGet(4).getNumWithDefaultValue(0);
        r1A = obj1.arrayGet(5).getNumWithDefaultValue(0);
    } else {
        error(errSyntaxWarning, -1, "Missing or invalid Coords in shading dictionary");
        return {};
    }

    t0A = 0;
    t1A = 1;
    obj1 = dict->lookup("Domain");
    if (obj1.isArrayOfLength(2)) {
        t0A = obj1.arrayGet(0).getNumWithDefaultValue(0);
        t1A = obj1.arrayGet(1).getNumWithDefaultValue(1);
    }

    obj1 = dict->lookup("Function");
    if (obj1.isArray()) {
        const int nFuncsA = obj1.arrayGetLength();
        if (nFuncsA > gfxColorMaxComps) {
            error(errSyntaxWarning, -1, "Invalid Function array in shading dictionary");
            return {};
        }
        for (i = 0; i < nFuncsA; ++i) {
            Object obj2 = obj1.arrayGet(i);
            std::unique_ptr<Function> f = Function::parse(&obj2);
            if (!f) {
                return {};
            }
            funcsA.emplace_back(std::move(f));
        }
    } else {
        std::unique_ptr<Function> f = Function::parse(&obj1);
        if (!f) {
            return {};
        }
        funcsA.emplace_back(std::move(f));
    }

    extend0A = extend1A = false;
    obj1 = dict->lookup("Extend");
    if (obj1.isArrayOfLength(2)) {
        extend0A = obj1.arrayGet(0).getBoolWithDefaultValue(false);
        extend1A = obj1.arrayGet(1).getBoolWithDefaultValue(false);
    }

    auto shading = std::make_unique<GfxRadialShading>(x0A, y0A, r0A, x1A, y1A, r1A, t0A, t1A, std::move(funcsA), extend0A, extend1A);
    if (!shading->init(res, dict, out, state)) {
        return {};
    }
    return shading;
}

std::unique_ptr<GfxShading> GfxRadialShading::copy() const
{
    return std::make_unique<GfxRadialShading>(this);
}

double GfxRadialShading::getDistance(double sMin, double sMax) const
{
    double xMin, yMin, rMin, xMax, yMax, rMax;

    xMin = x0 + sMin * (x1 - x0);
    yMin = y0 + sMin * (y1 - y0);
    rMin = r0 + sMin * (r1 - r0);

    xMax = x0 + sMax * (x1 - x0);
    yMax = y0 + sMax * (y1 - y0);
    rMax = r0 + sMax * (r1 - r0);

    return hypot(xMax - xMin, yMax - yMin) + fabs(rMax - rMin);
}

// extend range, adapted from cairo, radialExtendRange
static bool radialExtendRange(double range[2], double value, bool valid)
{
    if (!valid) {
        range[0] = range[1] = value;
    } else if (value < range[0]) {
        range[0] = value;
    } else if (value > range[1]) {
        range[1] = value;
    }

    return true;
}

inline void radialEdge(double num, double den, double delta, double lower, double upper, double dr, double mindr, bool &valid, double *range)
{
    if (fabs(den) >= RADIAL_EPSILON) {
        double t_edge, v;
        t_edge = num / den;
        v = t_edge * delta;
        if (t_edge * dr >= mindr && lower <= v && v <= upper) {
            valid = radialExtendRange(range, t_edge, valid);
        }
    }
}

inline void radialCorner1(double x, double y, double &b, double dx, double dy, double cr, double dr, double mindr, bool &valid, double *range)
{
    b = x * dx + y * dy + cr * dr;
    if (fabs(b) >= RADIAL_EPSILON) {
        double t_corner;
        double x2 = x * x;
        double y2 = y * y;
        double cr2 = cr * cr;
        double c = x2 + y2 - cr2;

        t_corner = 0.5 * c / b;
        if (t_corner * dr >= mindr) {
            valid = radialExtendRange(range, t_corner, valid);
        }
    }
}

inline void radialCorner2(double x, double y, double a, double &b, double &c, double &d, double dx, double dy, double cr, double inva, double dr, double mindr, bool &valid, double *range)
{
    b = x * dx + y * dy + cr * dr;
    c = x * x + y * y - cr * cr;
    d = b * b - a * c;
    if (d >= 0) {
        double t_corner;

        d = sqrt(d);
        t_corner = (b + d) * inva;
        if (t_corner * dr >= mindr) {
            valid = radialExtendRange(range, t_corner, valid);
        }
        t_corner = (b - d) * inva;
        if (t_corner * dr >= mindr) {
            valid = radialExtendRange(range, t_corner, valid);
        }
    }
}
void GfxRadialShading::getParameterRange(double *lower, double *upper, double xMin, double yMin, double xMax, double yMax)
{
    double cx, cy, cr, dx, dy, dr;
    double a, x_focus, y_focus;
    double mindr, minx, miny, maxx, maxy;
    double range[2];
    bool valid;

    // A radial pattern is considered degenerate if it can be
    // represented as a solid or clear pattern.  This corresponds to one
    // of the two cases:
    //
    // 1) The radii are both very small:
    //      |dr| < FLT_EPSILON && min (r0, r1) < FLT_EPSILON
    //
    // 2) The two circles have about the same radius and are very
    //    close to each other (approximately a cylinder gradient that
    //    doesn't move with the parameter):
    //      |dr| < FLT_EPSILON && max (|dx|, |dy|) < 2 * FLT_EPSILON

    if (xMin >= xMax || yMin >= yMax || (fabs(r0 - r1) < RADIAL_EPSILON && (std::min<double>(r0, r1) < RADIAL_EPSILON || std::max<double>(fabs(x0 - x1), fabs(y0 - y1)) < 2 * RADIAL_EPSILON))) {
        *lower = *upper = 0;
        return;
    }

    range[0] = range[1] = 0;
    valid = false;

    x_focus = y_focus = 0; // silence gcc

    cx = x0;
    cy = y0;
    cr = r0;
    dx = x1 - cx;
    dy = y1 - cy;
    dr = r1 - cr;

    // translate by -(cx, cy) to simplify computations
    xMin -= cx;
    yMin -= cy;
    xMax -= cx;
    yMax -= cy;

    // enlarge boundaries slightly to avoid rounding problems in the
    // parameter range computation
    xMin -= RADIAL_EPSILON;
    yMin -= RADIAL_EPSILON;
    xMax += RADIAL_EPSILON;
    yMax += RADIAL_EPSILON;

    // enlarge boundaries even more to avoid rounding problems when
    // testing if a point belongs to the box
    minx = xMin - RADIAL_EPSILON;
    miny = yMin - RADIAL_EPSILON;
    maxx = xMax + RADIAL_EPSILON;
    maxy = yMax + RADIAL_EPSILON;

    // we dont' allow negative radiuses, so we will be checking that
    // t*dr >= mindr to consider t valid
    mindr = -(cr + RADIAL_EPSILON);

    // After the previous transformations, the start circle is centered
    // in the origin and has radius cr. A 1-unit change in the t
    // parameter corresponds to dx,dy,dr changes in the x,y,r of the
    // circle (center coordinates, radius).
    //
    // To compute the minimum range needed to correctly draw the
    // pattern, we start with an empty range and extend it to include
    // the circles touching the bounding box or within it.

    // Focus, the point where the circle has radius == 0.
    //
    // r = cr + t * dr = 0
    // t = -cr / dr
    //
    // If the radius is constant (dr == 0) there is no focus (the
    // gradient represents a cylinder instead of a cone).
    if (fabs(dr) >= RADIAL_EPSILON) {
        double t_focus;

        t_focus = -cr / dr;
        x_focus = t_focus * dx;
        y_focus = t_focus * dy;
        if (minx <= x_focus && x_focus <= maxx && miny <= y_focus && y_focus <= maxy) {
            valid = radialExtendRange(range, t_focus, valid);
        }
    }

    // Circles externally tangent to box edges.
    //
    // All circles have center in (dx, dy) * t
    //
    // If the circle is tangent to the line defined by the edge of the
    // box, then at least one of the following holds true:
    //
    //   (dx*t) + (cr + dr*t) == x0 (left   edge)
    //   (dx*t) - (cr + dr*t) == x1 (right  edge)
    //   (dy*t) + (cr + dr*t) == y0 (top    edge)
    //   (dy*t) - (cr + dr*t) == y1 (bottom edge)
    //
    // The solution is only valid if the tangent point is actually on
    // the edge, i.e. if its y coordinate is in [y0,y1] for left/right
    // edges and if its x coordinate is in [x0,x1] for top/bottom edges.
    //
    // For the first equation:
    //
    //   (dx + dr) * t = x0 - cr
    //   t = (x0 - cr) / (dx + dr)
    //   y = dy * t
    //
    // in the code this becomes:
    //
    //   t_edge = (num) / (den)
    //   v = (delta) * t_edge
    //
    // If the denominator in t is 0, the pattern is tangent to a line
    // parallel to the edge under examination. The corner-case where the
    // boundary line is the same as the edge is handled by the focus
    // point case and/or by the a==0 case.

    // circles tangent (externally) to left/right/top/bottom edge
    radialEdge(xMin - cr, dx + dr, dy, miny, maxy, dr, mindr, valid, range);
    radialEdge(xMax + cr, dx - dr, dy, miny, maxy, dr, mindr, valid, range);
    radialEdge(yMin - cr, dy + dr, dx, minx, maxx, dr, mindr, valid, range);
    radialEdge(yMax + cr, dy - dr, dx, minx, maxx, dr, mindr, valid, range);

    // Circles passing through a corner.
    //
    // A circle passing through the point (x,y) satisfies:
    //
    // (x-t*dx)^2 + (y-t*dy)^2 == (cr + t*dr)^2
    //
    // If we set:
    //   a = dx^2 + dy^2 - dr^2
    //   b = x*dx + y*dy + cr*dr
    //   c = x^2 + y^2 - cr^2
    // we have:
    //   a*t^2 - 2*b*t + c == 0

    a = dx * dx + dy * dy - dr * dr;
    if (fabs(a) < RADIAL_EPSILON * RADIAL_EPSILON) {
        double b;

        // Ensure that gradients with both a and dr small are
        // considered degenerate.
        // The floating point version of the degeneracy test implemented
        // in _radial_pattern_is_degenerate() is:
        //
        //  1) The circles are practically the same size:
        //     |dr| < RADIAL_EPSILON
        //  AND
        //  2a) The circles are both very small:
        //      min (r0, r1) < RADIAL_EPSILON
        //   OR
        //  2b) The circles are very close to each other:
        //      max (|dx|, |dy|) < 2 * RADIAL_EPSILON
        //
        // Assuming that the gradient is not degenerate, we want to
        // show that |a| < RADIAL_EPSILON^2 implies |dr| >= RADIAL_EPSILON.
        //
        // If the gradient is not degenerate yet it has |dr| <
        // RADIAL_EPSILON, (2b) is false, thus:
        //
        //   max (|dx|, |dy|) >= 2*RADIAL_EPSILON
        // which implies:
        //   4*RADIAL_EPSILON^2 <= max (|dx|, |dy|)^2 <= dx^2 + dy^2
        //
        // From the definition of a, we get:
        //   a = dx^2 + dy^2 - dr^2 < RADIAL_EPSILON^2
        //   dx^2 + dy^2 - RADIAL_EPSILON^2 < dr^2
        //   3*RADIAL_EPSILON^2 < dr^2
        //
        // which is inconsistent with the hypotheses, thus |dr| <
        // RADIAL_EPSILON is false or the gradient is degenerate.

        assert(fabs(dr) >= RADIAL_EPSILON);

        // If a == 0, all the circles are tangent to a line in the
        // focus point. If this line is within the box extents, we
        // should add the circle with infinite radius, but this would
        // make the range unbounded. We will be limiting the range to
        // [0,1] anyway, so we simply add the biggest legitimate
        // circle (it happens for 0 or for 1).
        if (dr < 0) {
            valid = radialExtendRange(range, 0, valid);
        } else {
            valid = radialExtendRange(range, 1, valid);
        }

        // Nondegenerate, nonlimit circles passing through the corners.
        //
        // a == 0 && a*t^2 - 2*b*t + c == 0
        //
        // t = c / (2*b)
        //
        // The b == 0 case has just been handled, so we only have to
        // compute this if b != 0.

        // circles touching each corner
        radialCorner1(xMin, yMin, b, dx, dy, cr, dr, mindr, valid, range);
        radialCorner1(xMin, yMax, b, dx, dy, cr, dr, mindr, valid, range);
        radialCorner1(xMax, yMin, b, dx, dy, cr, dr, mindr, valid, range);
        radialCorner1(xMax, yMax, b, dx, dy, cr, dr, mindr, valid, range);
    } else {
        double inva, b, c, d;

        inva = 1 / a;

        // Nondegenerate, nonlimit circles passing through the corners.
        //
        // a != 0 && a*t^2 - 2*b*t + c == 0
        //
        // t = (b +- sqrt (b*b - a*c)) / a
        //
        // If the argument of sqrt() is negative, then no circle
        // passes through the corner.

        // circles touching each corner
        radialCorner2(xMin, yMin, a, b, c, d, dx, dy, cr, inva, dr, mindr, valid, range);
        radialCorner2(xMin, yMax, a, b, c, d, dx, dy, cr, inva, dr, mindr, valid, range);
        radialCorner2(xMax, yMin, a, b, c, d, dx, dy, cr, inva, dr, mindr, valid, range);
        radialCorner2(xMax, yMax, a, b, c, d, dx, dy, cr, inva, dr, mindr, valid, range);
    }

    *lower = std::max<double>(0., std::min<double>(1., range[0]));
    *upper = std::max<double>(0., std::min<double>(1., range[1]));
}

//------------------------------------------------------------------------
// GfxShadingBitBuf
//------------------------------------------------------------------------

class GfxShadingBitBuf
{
public:
    explicit GfxShadingBitBuf(Stream *strA);
    ~GfxShadingBitBuf();
    GfxShadingBitBuf(const GfxShadingBitBuf &) = delete;
    GfxShadingBitBuf &operator=(const GfxShadingBitBuf &) = delete;
    bool getBits(int n, unsigned int *val);
    void flushBits();

private:
    Stream *str;
    int bitBuf;
    int nBits;
};

GfxShadingBitBuf::GfxShadingBitBuf(Stream *strA)
{
    str = strA;
    (void)str->rewind();
    bitBuf = 0;
    nBits = 0;
}

GfxShadingBitBuf::~GfxShadingBitBuf()
{
    str->close();
}

bool GfxShadingBitBuf::getBits(int n, unsigned int *val)
{
    unsigned int x;

    if (nBits >= n) {
        x = (bitBuf >> (nBits - n)) & ((1 << n) - 1);
        nBits -= n;
    } else {
        x = 0;
        if (nBits > 0) {
            x = bitBuf & ((1 << nBits) - 1);
            n -= nBits;
            nBits = 0;
        }
        while (n > 0) {
            if ((bitBuf = str->getChar()) == EOF) {
                nBits = 0;
                return false;
            }
            if (n >= 8) {
                x = (x << 8) | bitBuf;
                n -= 8;
            } else {
                x = (x << n) | (bitBuf >> (8 - n));
                nBits = 8 - n;
                n = 0;
            }
        }
    }
    *val = x;
    return true;
}

void GfxShadingBitBuf::flushBits()
{
    bitBuf = 0;
    nBits = 0;
}

//------------------------------------------------------------------------
// GfxGouraudTriangleShading
//------------------------------------------------------------------------

GfxGouraudTriangleShading::GfxGouraudTriangleShading(int typeA, GfxGouraudVertex *verticesA, int nVerticesA, int (*trianglesA)[3], int nTrianglesA, std::vector<std::unique_ptr<Function>> &&funcsA)
    : GfxShading(typeA), funcs(std::move(funcsA))
{
    vertices = verticesA;
    nVertices = nVerticesA;
    triangles = trianglesA;
    nTriangles = nTrianglesA;
}

GfxGouraudTriangleShading::GfxGouraudTriangleShading(const GfxGouraudTriangleShading *shading) : GfxShading(shading)
{
    nVertices = shading->nVertices;
    vertices = static_cast<GfxGouraudVertex *>(gmallocn(nVertices, sizeof(GfxGouraudVertex)));
    memcpy(vertices, shading->vertices, nVertices * sizeof(GfxGouraudVertex));
    nTriangles = shading->nTriangles;
    triangles = static_cast<int (*)[3]>(gmallocn(nTriangles * 3, sizeof(int)));
    memcpy(triangles, shading->triangles, nTriangles * 3 * sizeof(int));
    for (const auto &f : shading->funcs) {
        funcs.emplace_back(f->copy());
    }
}

GfxGouraudTriangleShading::~GfxGouraudTriangleShading()
{
    gfree(vertices);
    gfree(triangles);
}

std::unique_ptr<GfxGouraudTriangleShading> GfxGouraudTriangleShading::parse(GfxResources *res, int typeA, Dict *dict, Stream *str, OutputDev *out, GfxState *gfxState)
{
    std::vector<std::unique_ptr<Function>> funcsA;
    int coordBits, compBits, flagBits, vertsPerRow, nRows;
    double xMin, xMax, yMin, yMax;
    double cMin[gfxColorMaxComps], cMax[gfxColorMaxComps];
    double xMul, yMul;
    double cMul[gfxColorMaxComps];
    GfxGouraudVertex *verticesA;
    int (*trianglesA)[3];
    int nComps, nVerticesA, nTrianglesA, vertSize, triSize;
    unsigned int x, y, flag;
    unsigned int c[gfxColorMaxComps];
    GfxShadingBitBuf *bitBuf;
    Object obj1;
    int i, j, k, state;

    obj1 = dict->lookup("BitsPerCoordinate");
    if (obj1.isInt()) {
        coordBits = obj1.getInt();
    } else {
        error(errSyntaxWarning, -1, "Missing or invalid BitsPerCoordinate in shading dictionary");
        return {};
    }
    if (unlikely(coordBits <= 0)) {
        error(errSyntaxWarning, -1, "Invalid BitsPerCoordinate in shading dictionary");
        return {};
    }
    obj1 = dict->lookup("BitsPerComponent");
    if (obj1.isInt()) {
        compBits = obj1.getInt();
    } else {
        error(errSyntaxWarning, -1, "Missing or invalid BitsPerComponent in shading dictionary");
        return {};
    }
    if (unlikely(compBits <= 0 || compBits > 31)) {
        error(errSyntaxWarning, -1, "Invalid BitsPerComponent in shading dictionary");
        return {};
    }
    flagBits = vertsPerRow = 0; // make gcc happy
    if (typeA == 4) {
        obj1 = dict->lookup("BitsPerFlag");
        if (obj1.isInt()) {
            flagBits = obj1.getInt();
        } else {
            error(errSyntaxWarning, -1, "Missing or invalid BitsPerFlag in shading dictionary");
            return {};
        }
    } else {
        obj1 = dict->lookup("VerticesPerRow");
        if (obj1.isInt()) {
            vertsPerRow = obj1.getInt();
        } else {
            error(errSyntaxWarning, -1, "Missing or invalid VerticesPerRow in shading dictionary");
            return {};
        }
    }
    obj1 = dict->lookup("Decode");
    if (obj1.isArrayOfLengthAtLeast(6)) {
        bool decodeOk = true;
        xMin = obj1.arrayGet(0).getNum(&decodeOk);
        xMax = obj1.arrayGet(1).getNum(&decodeOk);
        xMul = (xMax - xMin) / (pow(2.0, coordBits) - 1);
        yMin = obj1.arrayGet(2).getNum(&decodeOk);
        yMax = obj1.arrayGet(3).getNum(&decodeOk);
        yMul = (yMax - yMin) / (pow(2.0, coordBits) - 1);
        for (i = 0; 5 + 2 * i < obj1.arrayGetLength() && i < gfxColorMaxComps; ++i) {
            cMin[i] = obj1.arrayGet(4 + 2 * i).getNum(&decodeOk);
            cMax[i] = obj1.arrayGet(5 + 2 * i).getNum(&decodeOk);
            cMul[i] = (cMax[i] - cMin[i]) / static_cast<double>((1U << compBits) - 1);
        }
        nComps = i;

        if (!decodeOk) {
            error(errSyntaxWarning, -1, "Missing or invalid Decode array in shading dictionary");
            return {};
        }
    } else {
        error(errSyntaxWarning, -1, "Missing or invalid Decode array in shading dictionary");
        return {};
    }

    obj1 = dict->lookup("Function");
    if (!obj1.isNull()) {
        if (obj1.isArray()) {
            const int nFuncsA = obj1.arrayGetLength();
            if (nFuncsA > gfxColorMaxComps) {
                error(errSyntaxWarning, -1, "Invalid Function array in shading dictionary");
                return {};
            }
            for (i = 0; i < nFuncsA; ++i) {
                Object obj2 = obj1.arrayGet(i);
                std::unique_ptr<Function> f = Function::parse(&obj2);
                if (!f) {
                    return {};
                }
                funcsA.emplace_back(std::move(f));
            }
        } else {
            std::unique_ptr<Function> f = Function::parse(&obj1);
            if (!f) {
                return {};
            }
            funcsA.emplace_back(std::move(f));
        }
    }

    nVerticesA = nTrianglesA = 0;
    verticesA = nullptr;
    trianglesA = nullptr;
    vertSize = triSize = 0;
    state = 0;
    flag = 0; // make gcc happy
    bitBuf = new GfxShadingBitBuf(str);
    while (true) {
        if (typeA == 4) {
            if (!bitBuf->getBits(flagBits, &flag)) {
                break;
            }
        }
        if (!bitBuf->getBits(coordBits, &x) || !bitBuf->getBits(coordBits, &y)) {
            break;
        }
        for (i = 0; i < nComps; ++i) {
            if (!bitBuf->getBits(compBits, &c[i])) {
                break;
            }
        }
        if (i < nComps) {
            break;
        }
        if (nVerticesA == vertSize) {
            int oldVertSize = vertSize;
            vertSize = (vertSize == 0) ? 16 : 2 * vertSize;
            verticesA = static_cast<GfxGouraudVertex *>(greallocn_checkoverflow(verticesA, vertSize, sizeof(GfxGouraudVertex)));
            if (unlikely(!verticesA)) {
                error(errSyntaxWarning, -1, "GfxGouraudTriangleShading::parse: vertices size overflow");
                gfree(trianglesA);
                delete bitBuf;
                return nullptr;
            }
            memset(verticesA + oldVertSize, 0, (vertSize - oldVertSize) * sizeof(GfxGouraudVertex));
        }
        verticesA[nVerticesA].x = xMin + xMul * static_cast<double>(x);
        verticesA[nVerticesA].y = yMin + yMul * static_cast<double>(y);
        for (i = 0; i < nComps; ++i) {
            verticesA[nVerticesA].color.c[i] = dblToCol(cMin[i] + cMul[i] * static_cast<double>(c[i]));
        }
        ++nVerticesA;
        bitBuf->flushBits();
        if (typeA == 4) {
            if (state == 0 || state == 1) {
                ++state;
            } else if (state == 2 || flag > 0) {
                if (nTrianglesA == triSize) {
                    triSize = (triSize == 0) ? 16 : 2 * triSize;
                    trianglesA = static_cast<int (*)[3]>(greallocn(trianglesA, triSize * 3, sizeof(int)));
                }
                if (state == 2) {
                    trianglesA[nTrianglesA][0] = nVerticesA - 3;
                    trianglesA[nTrianglesA][1] = nVerticesA - 2;
                    trianglesA[nTrianglesA][2] = nVerticesA - 1;
                    ++state;
                } else if (flag == 1) {
                    trianglesA[nTrianglesA][0] = trianglesA[nTrianglesA - 1][1];
                    trianglesA[nTrianglesA][1] = trianglesA[nTrianglesA - 1][2];
                    trianglesA[nTrianglesA][2] = nVerticesA - 1;
                } else { // flag == 2
                    trianglesA[nTrianglesA][0] = trianglesA[nTrianglesA - 1][0];
                    trianglesA[nTrianglesA][1] = trianglesA[nTrianglesA - 1][2];
                    trianglesA[nTrianglesA][2] = nVerticesA - 1;
                }
                ++nTrianglesA;
            } else { // state == 3 && flag == 0
                state = 1;
            }
        }
    }
    delete bitBuf;
    if (typeA == 5 && nVerticesA > 0 && vertsPerRow > 0) {
        nRows = nVerticesA / vertsPerRow;
        nTrianglesA = (nRows - 1) * 2 * (vertsPerRow - 1);
        trianglesA = static_cast<int (*)[3]>(gmallocn_checkoverflow(nTrianglesA * 3, sizeof(int)));
        if (unlikely(!trianglesA)) {
            gfree(verticesA);
            return nullptr;
        }
        k = 0;
        for (i = 0; i < nRows - 1; ++i) {
            for (j = 0; j < vertsPerRow - 1; ++j) {
                trianglesA[k][0] = i * vertsPerRow + j;
                trianglesA[k][1] = i * vertsPerRow + j + 1;
                trianglesA[k][2] = (i + 1) * vertsPerRow + j;
                ++k;
                trianglesA[k][0] = i * vertsPerRow + j + 1;
                trianglesA[k][1] = (i + 1) * vertsPerRow + j;
                trianglesA[k][2] = (i + 1) * vertsPerRow + j + 1;
                ++k;
            }
        }
    }

    auto shading = std::make_unique<GfxGouraudTriangleShading>(typeA, verticesA, nVerticesA, trianglesA, nTrianglesA, std::move(funcsA));
    if (!shading->init(res, dict, out, gfxState)) {
        return {};
    }
    return shading;
}

bool GfxGouraudTriangleShading::init(GfxResources *res, Dict *dict, OutputDev *out, GfxState *state)
{
    const bool parentInit = GfxShading::init(res, dict, out, state);
    if (!parentInit) {
        return false;
    }

    // funcs needs to be one of the three:
    //  * One function 1-in -> nComps-out
    //  * nComps functions 1-in -> 1-out
    //  * empty
    const int nComps = colorSpace->getNComps();
    const int nFuncs = funcs.size();
    if (nFuncs == 1) {
        if (funcs[0]->getInputSize() != 1) {
            error(errSyntaxWarning, -1, "GfxGouraudTriangleShading: function with input size != 2");
            return false;
        }
        if (funcs[0]->getOutputSize() != nComps) {
            error(errSyntaxWarning, -1, "GfxGouraudTriangleShading: function with wrong output size");
            return false;
        }
    } else if (nFuncs == nComps) {
        for (const std::unique_ptr<Function> &f : funcs) {
            if (f->getInputSize() != 1) {
                error(errSyntaxWarning, -1, "GfxGouraudTriangleShading: function with input size != 2");
                return false;
            }
            if (f->getOutputSize() != 1) {
                error(errSyntaxWarning, -1, "GfxGouraudTriangleShading: function with wrong output size");
                return false;
            }
        }
    } else if (nFuncs != 0) {
        return false;
    }

    return true;
}

std::unique_ptr<GfxShading> GfxGouraudTriangleShading::copy() const
{
    return std::make_unique<GfxGouraudTriangleShading>(this);
}

void GfxGouraudTriangleShading::getTriangle(int i, double *x0, double *y0, GfxColor *color0, double *x1, double *y1, GfxColor *color1, double *x2, double *y2, GfxColor *color2) const
{
    int v;

    assert(!isParameterized());

    v = triangles[i][0];
    *x0 = vertices[v].x;
    *y0 = vertices[v].y;
    *color0 = vertices[v].color;
    v = triangles[i][1];
    *x1 = vertices[v].x;
    *y1 = vertices[v].y;
    *color1 = vertices[v].color;
    v = triangles[i][2];
    *x2 = vertices[v].x;
    *y2 = vertices[v].y;
    *color2 = vertices[v].color;
}

void GfxGouraudTriangleShading::getParameterizedColor(double t, GfxColor *color) const
{
    double out[gfxColorMaxComps];

    for (unsigned int j = 0; j < funcs.size(); ++j) {
        funcs[j]->transform(&t, &out[j]);
    }
    for (int j = 0; j < gfxColorMaxComps; ++j) {
        color->c[j] = dblToCol(out[j]);
    }
}

void GfxGouraudTriangleShading::getTriangle(int i, double *x0, double *y0, double *color0, double *x1, double *y1, double *color1, double *x2, double *y2, double *color2) const
{
    int v;

    assert(isParameterized());

    v = triangles[i][0];
    if (likely(v >= 0 && v < nVertices)) {
        *x0 = vertices[v].x;
        *y0 = vertices[v].y;
        *color0 = colToDbl(vertices[v].color.c[0]);
    }
    v = triangles[i][1];
    if (likely(v >= 0 && v < nVertices)) {
        *x1 = vertices[v].x;
        *y1 = vertices[v].y;
        *color1 = colToDbl(vertices[v].color.c[0]);
    }
    v = triangles[i][2];
    if (likely(v >= 0 && v < nVertices)) {
        *x2 = vertices[v].x;
        *y2 = vertices[v].y;
        *color2 = colToDbl(vertices[v].color.c[0]);
    }
}

//------------------------------------------------------------------------
// GfxPatchMeshShading
//------------------------------------------------------------------------

GfxPatchMeshShading::GfxPatchMeshShading(int typeA, GfxPatch *patchesA, int nPatchesA, std::vector<std::unique_ptr<Function>> &&funcsA) : GfxShading(typeA), funcs(std::move(funcsA))
{
    patches = patchesA;
    nPatches = nPatchesA;
}

GfxPatchMeshShading::GfxPatchMeshShading(const GfxPatchMeshShading *shading) : GfxShading(shading)
{
    nPatches = shading->nPatches;
    patches = static_cast<GfxPatch *>(gmallocn(nPatches, sizeof(GfxPatch)));
    memcpy(patches, shading->patches, nPatches * sizeof(GfxPatch));
    for (const auto &f : shading->funcs) {
        funcs.emplace_back(f->copy());
    }
}

GfxPatchMeshShading::~GfxPatchMeshShading()
{
    gfree(patches);
}

std::unique_ptr<GfxPatchMeshShading> GfxPatchMeshShading::parse(GfxResources *res, int typeA, Dict *dict, Stream *str, OutputDev *out, GfxState *state)
{
    std::vector<std::unique_ptr<Function>> funcsA;
    int coordBits, compBits, flagBits;
    double xMin, xMax, yMin, yMax;
    double cMin[gfxColorMaxComps], cMax[gfxColorMaxComps];
    double xMul, yMul;
    double cMul[gfxColorMaxComps];
    GfxPatch *patchesA, *p;
    int nComps, nPatchesA, patchesSize, nPts, nColors;
    unsigned int flag;
    double x[16], y[16];
    unsigned int xi, yi;
    double c[4][gfxColorMaxComps];
    unsigned int ci;
    Object obj1;
    int i, j;

    obj1 = dict->lookup("BitsPerCoordinate");
    if (obj1.isInt()) {
        coordBits = obj1.getInt();
    } else {
        error(errSyntaxWarning, -1, "Missing or invalid BitsPerCoordinate in shading dictionary");
        return {};
    }
    if (unlikely(coordBits <= 0)) {
        error(errSyntaxWarning, -1, "Invalid BitsPerCoordinate in shading dictionary");
        return {};
    }
    obj1 = dict->lookup("BitsPerComponent");
    if (obj1.isInt()) {
        compBits = obj1.getInt();
    } else {
        error(errSyntaxWarning, -1, "Missing or invalid BitsPerComponent in shading dictionary");
        return {};
    }
    if (unlikely(compBits <= 0 || compBits > 31)) {
        error(errSyntaxWarning, -1, "Invalid BitsPerComponent in shading dictionary");
        return {};
    }
    obj1 = dict->lookup("BitsPerFlag");
    if (obj1.isInt()) {
        flagBits = obj1.getInt();
    } else {
        error(errSyntaxWarning, -1, "Missing or invalid BitsPerFlag in shading dictionary");
        return {};
    }
    obj1 = dict->lookup("Decode");
    if (obj1.isArrayOfLengthAtLeast(6)) {
        bool decodeOk = true;
        xMin = obj1.arrayGet(0).getNum(&decodeOk);
        xMax = obj1.arrayGet(1).getNum(&decodeOk);
        xMul = (xMax - xMin) / (pow(2.0, coordBits) - 1);
        yMin = obj1.arrayGet(2).getNum(&decodeOk);
        yMax = obj1.arrayGet(3).getNum(&decodeOk);
        yMul = (yMax - yMin) / (pow(2.0, coordBits) - 1);
        for (i = 0; 5 + 2 * i < obj1.arrayGetLength() && i < gfxColorMaxComps; ++i) {
            cMin[i] = obj1.arrayGet(4 + 2 * i).getNum(&decodeOk);
            cMax[i] = obj1.arrayGet(5 + 2 * i).getNum(&decodeOk);
            cMul[i] = (cMax[i] - cMin[i]) / static_cast<double>((1U << compBits) - 1);
        }
        nComps = i;

        if (!decodeOk) {
            error(errSyntaxWarning, -1, "Missing or invalid Decode array in shading dictionary");
            return {};
        }
    } else {
        error(errSyntaxWarning, -1, "Missing or invalid Decode array in shading dictionary");
        return {};
    }

    obj1 = dict->lookup("Function");
    if (!obj1.isNull()) {
        if (obj1.isArray()) {
            const int nFuncsA = obj1.arrayGetLength();
            if (nFuncsA > gfxColorMaxComps) {
                error(errSyntaxWarning, -1, "Invalid Function array in shading dictionary");
                return {};
            }
            for (i = 0; i < nFuncsA; ++i) {
                Object obj2 = obj1.arrayGet(i);
                std::unique_ptr<Function> f = Function::parse(&obj2);
                if (!f) {
                    return {};
                }
                funcsA.emplace_back(std::move(f));
            }
        } else {
            std::unique_ptr<Function> f = Function::parse(&obj1);
            if (!f) {
                return {};
            }
            funcsA.emplace_back(std::move(f));
        }
    }

    nPatchesA = 0;
    patchesA = nullptr;
    patchesSize = 0;
    auto bitBuf = std::make_unique<GfxShadingBitBuf>(str);
    while (true) {
        if (!bitBuf->getBits(flagBits, &flag)) {
            break;
        }
        if (typeA == 6) {
            switch (flag) {
            case 0:
                nPts = 12;
                nColors = 4;
                break;
            case 1:
            case 2:
            case 3:
            default:
                nPts = 8;
                nColors = 2;
                break;
            }
        } else {
            switch (flag) {
            case 0:
                nPts = 16;
                nColors = 4;
                break;
            case 1:
            case 2:
            case 3:
            default:
                nPts = 12;
                nColors = 2;
                break;
            }
        }
        for (i = 0; i < nPts; ++i) {
            if (!bitBuf->getBits(coordBits, &xi) || !bitBuf->getBits(coordBits, &yi)) {
                break;
            }
            x[i] = xMin + xMul * static_cast<double>(xi);
            y[i] = yMin + yMul * static_cast<double>(yi);
        }
        if (i < nPts) {
            break;
        }
        for (i = 0; i < nColors; ++i) {
            for (j = 0; j < nComps; ++j) {
                if (!bitBuf->getBits(compBits, &ci)) {
                    break;
                }
                c[i][j] = cMin[j] + cMul[j] * static_cast<double>(ci);
                if (funcsA.empty()) {
                    // ... and colorspace values can also be stored into doubles.
                    // They will be casted later.
                    c[i][j] = dblToCol(c[i][j]);
                }
            }
            if (j < nComps) {
                break;
            }
        }
        if (i < nColors) {
            break;
        }
        if (nPatchesA == patchesSize) {
            int oldPatchesSize = patchesSize;
            patchesSize = (patchesSize == 0) ? 16 : 2 * patchesSize;
            patchesA = static_cast<GfxPatch *>(greallocn_checkoverflow(patchesA, patchesSize, sizeof(GfxPatch)));
            if (unlikely(!patchesA)) {
                return {};
            }
            memset(patchesA + oldPatchesSize, 0, (patchesSize - oldPatchesSize) * sizeof(GfxPatch));
        }
        p = &patchesA[nPatchesA];
        if (typeA == 6) {
            switch (flag) {
            case 0:
                p->x[0][0] = x[0];
                p->y[0][0] = y[0];
                p->x[0][1] = x[1];
                p->y[0][1] = y[1];
                p->x[0][2] = x[2];
                p->y[0][2] = y[2];
                p->x[0][3] = x[3];
                p->y[0][3] = y[3];
                p->x[1][3] = x[4];
                p->y[1][3] = y[4];
                p->x[2][3] = x[5];
                p->y[2][3] = y[5];
                p->x[3][3] = x[6];
                p->y[3][3] = y[6];
                p->x[3][2] = x[7];
                p->y[3][2] = y[7];
                p->x[3][1] = x[8];
                p->y[3][1] = y[8];
                p->x[3][0] = x[9];
                p->y[3][0] = y[9];
                p->x[2][0] = x[10];
                p->y[2][0] = y[10];
                p->x[1][0] = x[11];
                p->y[1][0] = y[11];
                for (j = 0; j < nComps; ++j) {
                    p->color[0][0].c[j] = c[0][j];
                    p->color[0][1].c[j] = c[1][j];
                    p->color[1][1].c[j] = c[2][j];
                    p->color[1][0].c[j] = c[3][j];
                }
                break;
            case 1:
                if (nPatchesA == 0) {
                    gfree(patchesA);
                    return nullptr;
                }
                p->x[0][0] = patchesA[nPatchesA - 1].x[0][3];
                p->y[0][0] = patchesA[nPatchesA - 1].y[0][3];
                p->x[0][1] = patchesA[nPatchesA - 1].x[1][3];
                p->y[0][1] = patchesA[nPatchesA - 1].y[1][3];
                p->x[0][2] = patchesA[nPatchesA - 1].x[2][3];
                p->y[0][2] = patchesA[nPatchesA - 1].y[2][3];
                p->x[0][3] = patchesA[nPatchesA - 1].x[3][3];
                p->y[0][3] = patchesA[nPatchesA - 1].y[3][3];
                p->x[1][3] = x[0];
                p->y[1][3] = y[0];
                p->x[2][3] = x[1];
                p->y[2][3] = y[1];
                p->x[3][3] = x[2];
                p->y[3][3] = y[2];
                p->x[3][2] = x[3];
                p->y[3][2] = y[3];
                p->x[3][1] = x[4];
                p->y[3][1] = y[4];
                p->x[3][0] = x[5];
                p->y[3][0] = y[5];
                p->x[2][0] = x[6];
                p->y[2][0] = y[6];
                p->x[1][0] = x[7];
                p->y[1][0] = y[7];
                for (j = 0; j < nComps; ++j) {
                    p->color[0][0].c[j] = patchesA[nPatchesA - 1].color[0][1].c[j];
                    p->color[0][1].c[j] = patchesA[nPatchesA - 1].color[1][1].c[j];
                    p->color[1][1].c[j] = c[0][j];
                    p->color[1][0].c[j] = c[1][j];
                }
                break;
            case 2:
                if (nPatchesA == 0) {
                    gfree(patchesA);
                    return {};
                }
                p->x[0][0] = patchesA[nPatchesA - 1].x[3][3];
                p->y[0][0] = patchesA[nPatchesA - 1].y[3][3];
                p->x[0][1] = patchesA[nPatchesA - 1].x[3][2];
                p->y[0][1] = patchesA[nPatchesA - 1].y[3][2];
                p->x[0][2] = patchesA[nPatchesA - 1].x[3][1];
                p->y[0][2] = patchesA[nPatchesA - 1].y[3][1];
                p->x[0][3] = patchesA[nPatchesA - 1].x[3][0];
                p->y[0][3] = patchesA[nPatchesA - 1].y[3][0];
                p->x[1][3] = x[0];
                p->y[1][3] = y[0];
                p->x[2][3] = x[1];
                p->y[2][3] = y[1];
                p->x[3][3] = x[2];
                p->y[3][3] = y[2];
                p->x[3][2] = x[3];
                p->y[3][2] = y[3];
                p->x[3][1] = x[4];
                p->y[3][1] = y[4];
                p->x[3][0] = x[5];
                p->y[3][0] = y[5];
                p->x[2][0] = x[6];
                p->y[2][0] = y[6];
                p->x[1][0] = x[7];
                p->y[1][0] = y[7];
                for (j = 0; j < nComps; ++j) {
                    p->color[0][0].c[j] = patchesA[nPatchesA - 1].color[1][1].c[j];
                    p->color[0][1].c[j] = patchesA[nPatchesA - 1].color[1][0].c[j];
                    p->color[1][1].c[j] = c[0][j];
                    p->color[1][0].c[j] = c[1][j];
                }
                break;
            case 3:
                if (nPatchesA == 0) {
                    gfree(patchesA);
                    return {};
                }
                p->x[0][0] = patchesA[nPatchesA - 1].x[3][0];
                p->y[0][0] = patchesA[nPatchesA - 1].y[3][0];
                p->x[0][1] = patchesA[nPatchesA - 1].x[2][0];
                p->y[0][1] = patchesA[nPatchesA - 1].y[2][0];
                p->x[0][2] = patchesA[nPatchesA - 1].x[1][0];
                p->y[0][2] = patchesA[nPatchesA - 1].y[1][0];
                p->x[0][3] = patchesA[nPatchesA - 1].x[0][0];
                p->y[0][3] = patchesA[nPatchesA - 1].y[0][0];
                p->x[1][3] = x[0];
                p->y[1][3] = y[0];
                p->x[2][3] = x[1];
                p->y[2][3] = y[1];
                p->x[3][3] = x[2];
                p->y[3][3] = y[2];
                p->x[3][2] = x[3];
                p->y[3][2] = y[3];
                p->x[3][1] = x[4];
                p->y[3][1] = y[4];
                p->x[3][0] = x[5];
                p->y[3][0] = y[5];
                p->x[2][0] = x[6];
                p->y[2][0] = y[6];
                p->x[1][0] = x[7];
                p->y[1][0] = y[7];
                for (j = 0; j < nComps; ++j) {
                    p->color[0][0].c[j] = patchesA[nPatchesA - 1].color[1][0].c[j];
                    p->color[0][1].c[j] = patchesA[nPatchesA - 1].color[0][0].c[j];
                    p->color[1][1].c[j] = c[0][j];
                    p->color[1][0].c[j] = c[1][j];
                }
                break;
            }
        } else {
            switch (flag) {
            case 0:
                p->x[0][0] = x[0];
                p->y[0][0] = y[0];
                p->x[0][1] = x[1];
                p->y[0][1] = y[1];
                p->x[0][2] = x[2];
                p->y[0][2] = y[2];
                p->x[0][3] = x[3];
                p->y[0][3] = y[3];
                p->x[1][3] = x[4];
                p->y[1][3] = y[4];
                p->x[2][3] = x[5];
                p->y[2][3] = y[5];
                p->x[3][3] = x[6];
                p->y[3][3] = y[6];
                p->x[3][2] = x[7];
                p->y[3][2] = y[7];
                p->x[3][1] = x[8];
                p->y[3][1] = y[8];
                p->x[3][0] = x[9];
                p->y[3][0] = y[9];
                p->x[2][0] = x[10];
                p->y[2][0] = y[10];
                p->x[1][0] = x[11];
                p->y[1][0] = y[11];
                p->x[1][1] = x[12];
                p->y[1][1] = y[12];
                p->x[1][2] = x[13];
                p->y[1][2] = y[13];
                p->x[2][2] = x[14];
                p->y[2][2] = y[14];
                p->x[2][1] = x[15];
                p->y[2][1] = y[15];
                for (j = 0; j < nComps; ++j) {
                    p->color[0][0].c[j] = c[0][j];
                    p->color[0][1].c[j] = c[1][j];
                    p->color[1][1].c[j] = c[2][j];
                    p->color[1][0].c[j] = c[3][j];
                }
                break;
            case 1:
                if (nPatchesA == 0) {
                    gfree(patchesA);
                    return {};
                }
                p->x[0][0] = patchesA[nPatchesA - 1].x[0][3];
                p->y[0][0] = patchesA[nPatchesA - 1].y[0][3];
                p->x[0][1] = patchesA[nPatchesA - 1].x[1][3];
                p->y[0][1] = patchesA[nPatchesA - 1].y[1][3];
                p->x[0][2] = patchesA[nPatchesA - 1].x[2][3];
                p->y[0][2] = patchesA[nPatchesA - 1].y[2][3];
                p->x[0][3] = patchesA[nPatchesA - 1].x[3][3];
                p->y[0][3] = patchesA[nPatchesA - 1].y[3][3];
                p->x[1][3] = x[0];
                p->y[1][3] = y[0];
                p->x[2][3] = x[1];
                p->y[2][3] = y[1];
                p->x[3][3] = x[2];
                p->y[3][3] = y[2];
                p->x[3][2] = x[3];
                p->y[3][2] = y[3];
                p->x[3][1] = x[4];
                p->y[3][1] = y[4];
                p->x[3][0] = x[5];
                p->y[3][0] = y[5];
                p->x[2][0] = x[6];
                p->y[2][0] = y[6];
                p->x[1][0] = x[7];
                p->y[1][0] = y[7];
                p->x[1][1] = x[8];
                p->y[1][1] = y[8];
                p->x[1][2] = x[9];
                p->y[1][2] = y[9];
                p->x[2][2] = x[10];
                p->y[2][2] = y[10];
                p->x[2][1] = x[11];
                p->y[2][1] = y[11];
                for (j = 0; j < nComps; ++j) {
                    p->color[0][0].c[j] = patchesA[nPatchesA - 1].color[0][1].c[j];
                    p->color[0][1].c[j] = patchesA[nPatchesA - 1].color[1][1].c[j];
                    p->color[1][1].c[j] = c[0][j];
                    p->color[1][0].c[j] = c[1][j];
                }
                break;
            case 2:
                if (nPatchesA == 0) {
                    gfree(patchesA);
                    return {};
                }
                p->x[0][0] = patchesA[nPatchesA - 1].x[3][3];
                p->y[0][0] = patchesA[nPatchesA - 1].y[3][3];
                p->x[0][1] = patchesA[nPatchesA - 1].x[3][2];
                p->y[0][1] = patchesA[nPatchesA - 1].y[3][2];
                p->x[0][2] = patchesA[nPatchesA - 1].x[3][1];
                p->y[0][2] = patchesA[nPatchesA - 1].y[3][1];
                p->x[0][3] = patchesA[nPatchesA - 1].x[3][0];
                p->y[0][3] = patchesA[nPatchesA - 1].y[3][0];
                p->x[1][3] = x[0];
                p->y[1][3] = y[0];
                p->x[2][3] = x[1];
                p->y[2][3] = y[1];
                p->x[3][3] = x[2];
                p->y[3][3] = y[2];
                p->x[3][2] = x[3];
                p->y[3][2] = y[3];
                p->x[3][1] = x[4];
                p->y[3][1] = y[4];
                p->x[3][0] = x[5];
                p->y[3][0] = y[5];
                p->x[2][0] = x[6];
                p->y[2][0] = y[6];
                p->x[1][0] = x[7];
                p->y[1][0] = y[7];
                p->x[1][1] = x[8];
                p->y[1][1] = y[8];
                p->x[1][2] = x[9];
                p->y[1][2] = y[9];
                p->x[2][2] = x[10];
                p->y[2][2] = y[10];
                p->x[2][1] = x[11];
                p->y[2][1] = y[11];
                for (j = 0; j < nComps; ++j) {
                    p->color[0][0].c[j] = patchesA[nPatchesA - 1].color[1][1].c[j];
                    p->color[0][1].c[j] = patchesA[nPatchesA - 1].color[1][0].c[j];
                    p->color[1][1].c[j] = c[0][j];
                    p->color[1][0].c[j] = c[1][j];
                }
                break;
            case 3:
                if (nPatchesA == 0) {
                    gfree(patchesA);
                    return {};
                }
                p->x[0][0] = patchesA[nPatchesA - 1].x[3][0];
                p->y[0][0] = patchesA[nPatchesA - 1].y[3][0];
                p->x[0][1] = patchesA[nPatchesA - 1].x[2][0];
                p->y[0][1] = patchesA[nPatchesA - 1].y[2][0];
                p->x[0][2] = patchesA[nPatchesA - 1].x[1][0];
                p->y[0][2] = patchesA[nPatchesA - 1].y[1][0];
                p->x[0][3] = patchesA[nPatchesA - 1].x[0][0];
                p->y[0][3] = patchesA[nPatchesA - 1].y[0][0];
                p->x[1][3] = x[0];
                p->y[1][3] = y[0];
                p->x[2][3] = x[1];
                p->y[2][3] = y[1];
                p->x[3][3] = x[2];
                p->y[3][3] = y[2];
                p->x[3][2] = x[3];
                p->y[3][2] = y[3];
                p->x[3][1] = x[4];
                p->y[3][1] = y[4];
                p->x[3][0] = x[5];
                p->y[3][0] = y[5];
                p->x[2][0] = x[6];
                p->y[2][0] = y[6];
                p->x[1][0] = x[7];
                p->y[1][0] = y[7];
                p->x[1][1] = x[8];
                p->y[1][1] = y[8];
                p->x[1][2] = x[9];
                p->y[1][2] = y[9];
                p->x[2][2] = x[10];
                p->y[2][2] = y[10];
                p->x[2][1] = x[11];
                p->y[2][1] = y[11];
                for (j = 0; j < nComps; ++j) {
                    p->color[0][0].c[j] = patchesA[nPatchesA - 1].color[1][0].c[j];
                    p->color[0][1].c[j] = patchesA[nPatchesA - 1].color[0][0].c[j];
                    p->color[1][1].c[j] = c[0][j];
                    p->color[1][0].c[j] = c[1][j];
                }
                break;
            }
        }
        ++nPatchesA;
        bitBuf->flushBits();
    }

    if (typeA == 6) {
        for (i = 0; i < nPatchesA; ++i) {
            p = &patchesA[i];
            p->x[1][1] = (-4 * p->x[0][0] + 6 * (p->x[0][1] + p->x[1][0]) - 2 * (p->x[0][3] + p->x[3][0]) + 3 * (p->x[3][1] + p->x[1][3]) - p->x[3][3]) / 9;
            p->y[1][1] = (-4 * p->y[0][0] + 6 * (p->y[0][1] + p->y[1][0]) - 2 * (p->y[0][3] + p->y[3][0]) + 3 * (p->y[3][1] + p->y[1][3]) - p->y[3][3]) / 9;
            p->x[1][2] = (-4 * p->x[0][3] + 6 * (p->x[0][2] + p->x[1][3]) - 2 * (p->x[0][0] + p->x[3][3]) + 3 * (p->x[3][2] + p->x[1][0]) - p->x[3][0]) / 9;
            p->y[1][2] = (-4 * p->y[0][3] + 6 * (p->y[0][2] + p->y[1][3]) - 2 * (p->y[0][0] + p->y[3][3]) + 3 * (p->y[3][2] + p->y[1][0]) - p->y[3][0]) / 9;
            p->x[2][1] = (-4 * p->x[3][0] + 6 * (p->x[3][1] + p->x[2][0]) - 2 * (p->x[3][3] + p->x[0][0]) + 3 * (p->x[0][1] + p->x[2][3]) - p->x[0][3]) / 9;
            p->y[2][1] = (-4 * p->y[3][0] + 6 * (p->y[3][1] + p->y[2][0]) - 2 * (p->y[3][3] + p->y[0][0]) + 3 * (p->y[0][1] + p->y[2][3]) - p->y[0][3]) / 9;
            p->x[2][2] = (-4 * p->x[3][3] + 6 * (p->x[3][2] + p->x[2][3]) - 2 * (p->x[3][0] + p->x[0][3]) + 3 * (p->x[0][2] + p->x[2][0]) - p->x[0][0]) / 9;
            p->y[2][2] = (-4 * p->y[3][3] + 6 * (p->y[3][2] + p->y[2][3]) - 2 * (p->y[3][0] + p->y[0][3]) + 3 * (p->y[0][2] + p->y[2][0]) - p->y[0][0]) / 9;
        }
    }

    auto shading = std::make_unique<GfxPatchMeshShading>(typeA, patchesA, nPatchesA, std::move(funcsA));
    if (!shading->init(res, dict, out, state)) {
        return {};
    }
    return shading;
}

bool GfxPatchMeshShading::init(GfxResources *res, Dict *dict, OutputDev *out, GfxState *state)
{
    const bool parentInit = GfxShading::init(res, dict, out, state);
    if (!parentInit) {
        return false;
    }

    // funcs needs to be one of the three:
    //  * One function 1-in -> nComps-out
    //  * nComps functions 1-in -> 1-out
    //  * empty
    const int nComps = colorSpace->getNComps();
    const int nFuncs = funcs.size();
    if (nFuncs == 1) {
        if (funcs[0]->getInputSize() != 1) {
            error(errSyntaxWarning, -1, "GfxPatchMeshShading: function with input size != 2");
            return false;
        }
        if (funcs[0]->getOutputSize() != nComps) {
            error(errSyntaxWarning, -1, "GfxPatchMeshShading: function with wrong output size");
            return false;
        }
    } else if (nFuncs == nComps) {
        for (const std::unique_ptr<Function> &f : funcs) {
            if (f->getInputSize() != 1) {
                error(errSyntaxWarning, -1, "GfxPatchMeshShading: function with input size != 2");
                return false;
            }
            if (f->getOutputSize() != 1) {
                error(errSyntaxWarning, -1, "GfxPatchMeshShading: function with wrong output size");
                return false;
            }
        }
    } else if (nFuncs != 0) {
        return false;
    }

    return true;
}

void GfxPatchMeshShading::getParameterizedColor(double t, GfxColor *color) const
{
    double out[gfxColorMaxComps] = {};

    for (unsigned int j = 0; j < funcs.size(); ++j) {
        funcs[j]->transform(&t, &out[j]);
    }
    for (int j = 0; j < gfxColorMaxComps; ++j) {
        color->c[j] = dblToCol(out[j]);
    }
}

std::unique_ptr<GfxShading> GfxPatchMeshShading::copy() const
{
    return std::make_unique<GfxPatchMeshShading>(this);
}

//------------------------------------------------------------------------
// GfxImageColorMap
//------------------------------------------------------------------------

GfxImageColorMap::GfxImageColorMap(int bitsA, Object *decode, std::unique_ptr<GfxColorSpace> &&colorSpaceA) : colorSpace(std::move(colorSpaceA))
{
    int maxPixel, indexHigh;
    unsigned char *indexedLookup;
    const Function *sepFunc;
    double x[gfxColorMaxComps];
    double y[gfxColorMaxComps] = {};
    int i, j, k;
    double mapped;
    bool useByteLookup;

    ok = true;
    useMatte = false;

    // initialize
    for (k = 0; k < gfxColorMaxComps; ++k) {
        lookup[k] = nullptr;
        lookup2[k] = nullptr;
    }
    byte_lookup = nullptr;

    // bits per component and color space
    if (unlikely(bitsA <= 0 || bitsA > 30)) {
        goto err1;
    }

    bits = bitsA;
    maxPixel = (1 << bits) - 1;

    // this is a hack to support 16 bits images, everywhere
    // we assume a component fits in 8 bits, with this hack
    // we treat 16 bit images as 8 bit ones until it's fixed correctly.
    // The hack has another part on ImageStream::getLine
    if (maxPixel > 255) {
        maxPixel = 255;
    }

    // get decode map
    if (decode->isNull()) {
        nComps = colorSpace->getNComps();
        colorSpace->getDefaultRanges(decodeLow, decodeRange, maxPixel);
    } else if (decode->isArray()) {
        nComps = decode->arrayGetLength() / 2;
        if (nComps < colorSpace->getNComps()) {
            goto err1;
        }
        if (nComps > colorSpace->getNComps()) {
            error(errSyntaxWarning, -1, "Too many elements in Decode array");
            nComps = colorSpace->getNComps();
        }
        for (i = 0; i < nComps; ++i) {
            Object obj = decode->arrayGet(2 * i);
            if (!obj.isNum()) {
                goto err1;
            }
            decodeLow[i] = obj.getNum();
            obj = decode->arrayGet(2 * i + 1);
            if (!obj.isNum()) {
                goto err1;
            }
            decodeRange[i] = obj.getNum() - decodeLow[i];
        }
    } else {
        goto err1;
    }

    // Construct a lookup table -- this stores pre-computed decoded
    // values for each component, i.e., the result of applying the
    // decode mapping to each possible image pixel component value.
    for (k = 0; k < nComps; ++k) {
        lookup[k] = static_cast<GfxColorComp *>(gmallocn(maxPixel + 1, sizeof(GfxColorComp)));
        for (i = 0; i <= maxPixel; ++i) {
            lookup[k][i] = dblToCol(decodeLow[k] + (i * decodeRange[k]) / maxPixel);
        }
    }

    // Optimization: for Indexed and Separation color spaces (which have
    // only one component), we pre-compute a second lookup table with
    // color values
    colorSpace2 = nullptr;
    nComps2 = 0;
    useByteLookup = false;
    switch (colorSpace->getMode()) {
    case csIndexed: {
        // Note that indexHigh may not be the same as maxPixel --
        // Distiller will remove unused palette entries, resulting in
        // indexHigh < maxPixel.
        auto *indexedCS = static_cast<GfxIndexedColorSpace *>(colorSpace.get());
        colorSpace2 = indexedCS->getBase();
        indexHigh = indexedCS->getIndexHigh();
        nComps2 = colorSpace2->getNComps();
        indexedLookup = indexedCS->getLookup();
        colorSpace2->getDefaultRanges(x, y, indexHigh);
        if (colorSpace2->useGetGrayLine() || colorSpace2->useGetRGBLine() || colorSpace2->useGetCMYKLine() || colorSpace2->useGetDeviceNLine()) {
            byte_lookup = static_cast<unsigned char *>(gmallocn((maxPixel + 1), nComps2));
            useByteLookup = true;
        }
        for (k = 0; k < nComps2; ++k) {
            lookup2[k] = static_cast<GfxColorComp *>(gmallocn(maxPixel + 1, sizeof(GfxColorComp)));
            for (i = 0; i <= maxPixel; ++i) {
                j = static_cast<int>(decodeLow[0] + (i * decodeRange[0]) / maxPixel + 0.5);
                if (j < 0) {
                    j = 0;
                } else if (j > indexHigh) {
                    j = indexHigh;
                }

                mapped = x[k] + (indexedLookup[j * nComps2 + k] / 255.0) * y[k];
                lookup2[k][i] = dblToCol(mapped);
                if (useByteLookup) {
                    byte_lookup[i * nComps2 + k] = static_cast<unsigned char>(mapped * 255);
                }
            }
        }
        break;
    }
    case csSeparation: {
        auto *sepCS = static_cast<GfxSeparationColorSpace *>(colorSpace.get());
        colorSpace2 = sepCS->getAlt();
        nComps2 = colorSpace2->getNComps();
        sepFunc = sepCS->getFunc();
        if (colorSpace2->useGetGrayLine() || colorSpace2->useGetRGBLine() || colorSpace2->useGetCMYKLine() || colorSpace2->useGetDeviceNLine()) {
            byte_lookup = static_cast<unsigned char *>(gmallocn((maxPixel + 1), nComps2));
            useByteLookup = true;
        }
        for (k = 0; k < nComps2; ++k) {
            lookup2[k] = static_cast<GfxColorComp *>(gmallocn(maxPixel + 1, sizeof(GfxColorComp)));
            for (i = 0; i <= maxPixel; ++i) {
                x[0] = decodeLow[0] + (i * decodeRange[0]) / maxPixel;
                sepFunc->transform(x, y);
                lookup2[k][i] = dblToCol(y[k]);
                if (useByteLookup) {
                    byte_lookup[i * nComps2 + k] = static_cast<unsigned char>(y[k] * 255);
                }
            }
        }
        break;
    }
    default:
        if ((!decode->isNull() || maxPixel != 255) && (colorSpace->useGetGrayLine() || (colorSpace->useGetRGBLine() && !decode->isNull()) || colorSpace->useGetCMYKLine() || colorSpace->useGetDeviceNLine())) {
            byte_lookup = static_cast<unsigned char *>(gmallocn((maxPixel + 1), nComps));
            useByteLookup = true;
        }
        for (k = 0; k < nComps; ++k) {
            lookup2[k] = static_cast<GfxColorComp *>(gmallocn(maxPixel + 1, sizeof(GfxColorComp)));
            for (i = 0; i <= maxPixel; ++i) {
                mapped = decodeLow[k] + (i * decodeRange[k]) / maxPixel;
                lookup2[k][i] = dblToCol(mapped);
                if (useByteLookup) {
                    int byte;

                    byte = static_cast<int>(mapped * 255.0 + 0.5);
                    if (byte < 0) {
                        byte = 0;
                    } else if (byte > 255) {
                        byte = 255;
                    }
                    byte_lookup[i * nComps + k] = byte;
                }
            }
        }
    }

    return;

err1:
    ok = false;
}

GfxImageColorMap::GfxImageColorMap(const GfxImageColorMap *colorMap)
{
    int n, i, k;

    colorSpace = colorMap->colorSpace->copy();
    bits = colorMap->bits;
    nComps = colorMap->nComps;
    nComps2 = colorMap->nComps2;
    useMatte = colorMap->useMatte;
    matteColor = colorMap->matteColor;
    colorSpace2 = nullptr;
    for (k = 0; k < gfxColorMaxComps; ++k) {
        lookup[k] = nullptr;
        lookup2[k] = nullptr;
    }
    byte_lookup = nullptr;
    n = 1 << bits;
    for (k = 0; k < nComps; ++k) {
        lookup[k] = static_cast<GfxColorComp *>(gmallocn(n, sizeof(GfxColorComp)));
        memcpy(lookup[k], colorMap->lookup[k], n * sizeof(GfxColorComp));
    }
    if (colorSpace->getMode() == csIndexed) {
        colorSpace2 = (static_cast<GfxIndexedColorSpace *>(colorSpace.get()))->getBase();
        for (k = 0; k < nComps2; ++k) {
            lookup2[k] = static_cast<GfxColorComp *>(gmallocn(n, sizeof(GfxColorComp)));
            memcpy(lookup2[k], colorMap->lookup2[k], n * sizeof(GfxColorComp));
        }
    } else if (colorSpace->getMode() == csSeparation) {
        colorSpace2 = (static_cast<GfxSeparationColorSpace *>(colorSpace.get()))->getAlt();
        for (k = 0; k < nComps2; ++k) {
            lookup2[k] = static_cast<GfxColorComp *>(gmallocn(n, sizeof(GfxColorComp)));
            memcpy(lookup2[k], colorMap->lookup2[k], n * sizeof(GfxColorComp));
        }
    } else {
        for (k = 0; k < nComps; ++k) {
            lookup2[k] = static_cast<GfxColorComp *>(gmallocn(n, sizeof(GfxColorComp)));
            memcpy(lookup2[k], colorMap->lookup2[k], n * sizeof(GfxColorComp));
        }
    }
    if (colorMap->byte_lookup) {
        int nc = colorSpace2 ? nComps2 : nComps;

        byte_lookup = static_cast<unsigned char *>(gmallocn(n, nc));
        memcpy(byte_lookup, colorMap->byte_lookup, n * nc);
    }
    for (i = 0; i < nComps; ++i) {
        decodeLow[i] = colorMap->decodeLow[i];
        decodeRange[i] = colorMap->decodeRange[i];
    }
    ok = true;
}

GfxImageColorMap::~GfxImageColorMap()
{
    int i;

    for (i = 0; i < gfxColorMaxComps; ++i) {
        gfree(lookup[i]);
        gfree(lookup2[i]);
    }
    gfree(byte_lookup);
}

void GfxImageColorMap::getGray(const unsigned char *x, GfxGray *gray) const
{
    GfxColor color;
    int i;

    if (colorSpace2) {
        for (i = 0; i < nComps2; ++i) {
            color.c[i] = lookup2[i][x[0]];
        }
        colorSpace2->getGray(color, gray);
    } else {
        for (i = 0; i < nComps; ++i) {
            color.c[i] = lookup2[i][x[i]];
        }
        colorSpace->getGray(color, gray);
    }
}

void GfxImageColorMap::getRGB(const unsigned char *x, GfxRGB *rgb)
{
    GfxColor color;
    int i;

    if (colorSpace2) {
        for (i = 0; i < nComps2; ++i) {
            color.c[i] = lookup2[i][x[0]];
        }
        colorSpace2->getRGB(color, rgb);
    } else {
        for (i = 0; i < nComps; ++i) {
            color.c[i] = lookup2[i][x[i]];
        }
        colorSpace->getRGB(color, rgb);
    }
}

void GfxImageColorMap::getGrayLine(unsigned char *in, unsigned char *out, int length)
{
    int i, j;
    unsigned char *inp, *tmp_line;

    if ((colorSpace2 && !colorSpace2->useGetGrayLine()) || (!colorSpace2 && !colorSpace->useGetGrayLine())) {
        GfxGray gray;

        inp = in;
        for (i = 0; i < length; i++) {
            getGray(inp, &gray);
            out[i] = colToByte(gray);
            inp += nComps;
        }
        return;
    }

    switch (colorSpace->getMode()) {
    case csIndexed:
    case csSeparation:
        tmp_line = static_cast<unsigned char *>(gmallocn(length, nComps2));
        for (i = 0; i < length; i++) {
            for (j = 0; j < nComps2; j++) {
                unsigned char c = in[i];
                if (byte_lookup) {
                    c = byte_lookup[c * nComps2 + j];
                }
                tmp_line[i * nComps2 + j] = c;
            }
        }
        colorSpace2->getGrayLine(tmp_line, out, length);
        gfree(tmp_line);
        break;

    default:
        if (byte_lookup) {
            inp = in;
            for (j = 0; j < length; j++) {
                for (i = 0; i < nComps; i++) {
                    *inp = byte_lookup[*inp * nComps + i];
                    inp++;
                }
            }
        }
        colorSpace->getGrayLine(in, out, length);
        break;
    }
}

void GfxImageColorMap::getRGBLine(unsigned char *in, unsigned int *out, int length)
{
    int i, j;
    unsigned char *inp, *tmp_line;

    if (!useRGBLine()) {
        GfxRGB rgb;

        inp = in;
        for (i = 0; i < length; i++) {
            getRGB(inp, &rgb);
            out[i] = (static_cast<int>(colToByte(rgb.r)) << 16) | (static_cast<int>(colToByte(rgb.g)) << 8) | (static_cast<int>(colToByte(rgb.b)) << 0);
            inp += nComps;
        }
        return;
    }

    switch (colorSpace->getMode()) {
    case csIndexed:
    case csSeparation:
        tmp_line = static_cast<unsigned char *>(gmallocn(length, nComps2));
        for (i = 0; i < length; i++) {
            for (j = 0; j < nComps2; j++) {
                unsigned char c = in[i];
                if (byte_lookup) {
                    c = byte_lookup[c * nComps2 + j];
                }
                tmp_line[i * nComps2 + j] = c;
            }
        }
        colorSpace2->getRGBLine(tmp_line, out, length);
        gfree(tmp_line);
        break;

    default:
        if (byte_lookup) {
            inp = in;
            for (j = 0; j < length; j++) {
                for (i = 0; i < nComps; i++) {
                    *inp = byte_lookup[*inp * nComps + i];
                    inp++;
                }
            }
        }
        colorSpace->getRGBLine(in, out, length);
        break;
    }
}

void GfxImageColorMap::getRGBLine(unsigned char *in, unsigned char *out, int length)
{
    int i, j;
    unsigned char *inp, *tmp_line;

    if (!useRGBLine()) {
        GfxRGB rgb;

        inp = in;
        for (i = 0; i < length; i++) {
            getRGB(inp, &rgb);
            *out++ = colToByte(rgb.r);
            *out++ = colToByte(rgb.g);
            *out++ = colToByte(rgb.b);
            inp += nComps;
        }
        return;
    }

    switch (colorSpace->getMode()) {
    case csIndexed:
    case csSeparation:
        tmp_line = static_cast<unsigned char *>(gmallocn(length, nComps2));
        for (i = 0; i < length; i++) {
            for (j = 0; j < nComps2; j++) {
                unsigned char c = in[i];
                if (byte_lookup) {
                    c = byte_lookup[c * nComps2 + j];
                }
                tmp_line[i * nComps2 + j] = c;
            }
        }
        colorSpace2->getRGBLine(tmp_line, out, length);
        gfree(tmp_line);
        break;

    default:
        if (byte_lookup) {
            inp = in;
            for (j = 0; j < length; j++) {
                for (i = 0; i < nComps; i++) {
                    *inp = byte_lookup[*inp * nComps + i];
                    inp++;
                }
            }
        }
        colorSpace->getRGBLine(in, out, length);
        break;
    }
}

void GfxImageColorMap::getRGBXLine(unsigned char *in, unsigned char *out, int length)
{
    int i, j;
    unsigned char *inp, *tmp_line;

    if (!useRGBLine()) {
        GfxRGB rgb;

        inp = in;
        for (i = 0; i < length; i++) {
            getRGB(inp, &rgb);
            *out++ = colToByte(rgb.r);
            *out++ = colToByte(rgb.g);
            *out++ = colToByte(rgb.b);
            *out++ = 255;
            inp += nComps;
        }
        return;
    }

    switch (colorSpace->getMode()) {
    case csIndexed:
    case csSeparation:
        tmp_line = static_cast<unsigned char *>(gmallocn(length, nComps2));
        for (i = 0; i < length; i++) {
            for (j = 0; j < nComps2; j++) {
                unsigned char c = in[i];
                if (byte_lookup) {
                    c = byte_lookup[c * nComps2 + j];
                }
                tmp_line[i * nComps2 + j] = c;
            }
        }
        colorSpace2->getRGBXLine(tmp_line, out, length);
        gfree(tmp_line);
        break;

    default:
        if (byte_lookup) {
            inp = in;
            for (j = 0; j < length; j++) {
                for (i = 0; i < nComps; i++) {
                    *inp = byte_lookup[*inp * nComps + i];
                    inp++;
                }
            }
        }
        colorSpace->getRGBXLine(in, out, length);
        break;
    }
}

void GfxImageColorMap::getCMYKLine(unsigned char *in, unsigned char *out, int length)
{
    int i, j;
    unsigned char *inp, *tmp_line;

    if (!useCMYKLine()) {
        GfxCMYK cmyk;

        inp = in;
        for (i = 0; i < length; i++) {
            getCMYK(inp, &cmyk);
            *out++ = colToByte(cmyk.c);
            *out++ = colToByte(cmyk.m);
            *out++ = colToByte(cmyk.y);
            *out++ = colToByte(cmyk.k);
            inp += nComps;
        }
        return;
    }

    switch (colorSpace->getMode()) {
    case csIndexed:
    case csSeparation:
        tmp_line = static_cast<unsigned char *>(gmallocn(length, nComps2));
        for (i = 0; i < length; i++) {
            for (j = 0; j < nComps2; j++) {
                unsigned char c = in[i];
                if (byte_lookup) {
                    c = byte_lookup[c * nComps2 + j];
                }
                tmp_line[i * nComps2 + j] = c;
            }
        }
        colorSpace2->getCMYKLine(tmp_line, out, length);
        gfree(tmp_line);
        break;

    default:
        if (byte_lookup) {
            inp = in;
            for (j = 0; j < length; j++) {
                for (i = 0; i < nComps; i++) {
                    *inp = byte_lookup[*inp * nComps + i];
                    inp++;
                }
            }
        }
        colorSpace->getCMYKLine(in, out, length);
        break;
    }
}

void GfxImageColorMap::getDeviceNLine(unsigned char *in, unsigned char *out, int length)
{
    unsigned char *inp, *tmp_line;

    if (!useDeviceNLine()) {
        GfxColor deviceN;

        inp = in;
        for (int i = 0; i < length; i++) {
            getDeviceN(inp, &deviceN);
            for (int j = 0; j < SPOT_NCOMPS + 4; j++) {
                *out++ = deviceN.c[j];
            }
            inp += nComps;
        }
        return;
    }

    switch (colorSpace->getMode()) {
    case csIndexed:
    case csSeparation:
        tmp_line = static_cast<unsigned char *>(gmallocn(length, nComps2));
        for (int i = 0; i < length; i++) {
            for (int j = 0; j < nComps2; j++) {
                unsigned char c = in[i];
                if (byte_lookup) {
                    c = byte_lookup[c * nComps2 + j];
                }
                tmp_line[i * nComps2 + j] = c;
            }
        }
        colorSpace2->getDeviceNLine(tmp_line, out, length);
        gfree(tmp_line);
        break;

    default:
        if (byte_lookup) {
            inp = in;
            for (int j = 0; j < length; j++) {
                for (int i = 0; i < nComps; i++) {
                    *inp = byte_lookup[*inp * nComps + i];
                    inp++;
                }
            }
        }
        colorSpace->getDeviceNLine(in, out, length);
        break;
    }
}

void GfxImageColorMap::getCMYK(const unsigned char *x, GfxCMYK *cmyk) const
{
    GfxColor color;
    int i;

    if (colorSpace2) {
        for (i = 0; i < nComps2; ++i) {
            color.c[i] = lookup2[i][x[0]];
        }
        colorSpace2->getCMYK(color, cmyk);
    } else {
        for (i = 0; i < nComps; ++i) {
            color.c[i] = lookup[i][x[i]];
        }
        colorSpace->getCMYK(color, cmyk);
    }
}

void GfxImageColorMap::getDeviceN(const unsigned char *x, GfxColor *deviceN)
{
    GfxColor color;
    int i;

    if (colorSpace2 && (colorSpace->getMapping().empty() || colorSpace->getMapping()[0] == -1)) {
        for (i = 0; i < nComps2; ++i) {
            color.c[i] = lookup2[i][x[0]];
        }
        colorSpace2->getDeviceN(color, deviceN);
    } else {
        for (i = 0; i < nComps; ++i) {
            color.c[i] = lookup[i][x[i]];
        }
        colorSpace->getDeviceN(color, deviceN);
    }
}

void GfxImageColorMap::getColor(const unsigned char *x, GfxColor *color)
{
    int maxPixel, i;

    maxPixel = (1 << bits) - 1;
    for (i = 0; i < nComps; ++i) {
        color->c[i] = dblToCol(decodeLow[i] + (x[i] * decodeRange[i]) / maxPixel);
    }
}

//------------------------------------------------------------------------
// GfxSubpath and GfxPath
//------------------------------------------------------------------------

GfxSubpath::GfxSubpath(double x1, double y1)
{
    size = 16;
    x = static_cast<double *>(gmallocn(size, sizeof(double)));
    y = static_cast<double *>(gmallocn(size, sizeof(double)));
    curve = static_cast<bool *>(gmallocn(size, sizeof(bool)));
    n = 1;
    x[0] = x1;
    y[0] = y1;
    curve[0] = false;
    closed = false;
}

GfxSubpath::~GfxSubpath()
{
    gfree(x);
    gfree(y);
    gfree(curve);
}

// Used for copy().
GfxSubpath::GfxSubpath(const GfxSubpath *subpath)
{
    size = subpath->size;
    n = subpath->n;
    x = static_cast<double *>(gmallocn(size, sizeof(double)));
    y = static_cast<double *>(gmallocn(size, sizeof(double)));
    curve = static_cast<bool *>(gmallocn(size, sizeof(bool)));
    memcpy(x, subpath->x, n * sizeof(double));
    memcpy(y, subpath->y, n * sizeof(double));
    memcpy(curve, subpath->curve, n * sizeof(bool));
    closed = subpath->closed;
}

void GfxSubpath::lineTo(double x1, double y1)
{
    if (n >= size) {
        size *= 2;
        x = static_cast<double *>(greallocn(x, size, sizeof(double)));
        y = static_cast<double *>(greallocn(y, size, sizeof(double)));
        curve = static_cast<bool *>(greallocn(curve, size, sizeof(bool)));
    }
    x[n] = x1;
    y[n] = y1;
    curve[n] = false;
    ++n;
}

void GfxSubpath::curveTo(double x1, double y1, double x2, double y2, double x3, double y3)
{
    if (n + 3 > size) {
        size *= 2;
        x = static_cast<double *>(greallocn(x, size, sizeof(double)));
        y = static_cast<double *>(greallocn(y, size, sizeof(double)));
        curve = static_cast<bool *>(greallocn(curve, size, sizeof(bool)));
    }
    x[n] = x1;
    y[n] = y1;
    x[n + 1] = x2;
    y[n + 1] = y2;
    x[n + 2] = x3;
    y[n + 2] = y3;
    curve[n] = curve[n + 1] = true;
    curve[n + 2] = false;
    n += 3;
}

void GfxSubpath::close()
{
    if (x[n - 1] != x[0] || y[n - 1] != y[0]) {
        lineTo(x[0], y[0]);
    }
    closed = true;
}

void GfxSubpath::offset(double dx, double dy)
{
    int i;

    for (i = 0; i < n; ++i) {
        x[i] += dx;
        y[i] += dy;
    }
}

GfxPath::GfxPath()
{
    justMoved = false;
    size = 16;
    n = 0;
    firstX = firstY = 0;
    subpaths = static_cast<GfxSubpath **>(gmallocn(size, sizeof(GfxSubpath *)));
}

GfxPath::~GfxPath()
{
    int i;

    for (i = 0; i < n; ++i) {
        delete subpaths[i];
    }
    gfree(static_cast<void *>(subpaths));
}

// Used for copy().
GfxPath::GfxPath(bool justMoved1, double firstX1, double firstY1, GfxSubpath **subpaths1, int n1, int size1)
{
    int i;

    justMoved = justMoved1;
    firstX = firstX1;
    firstY = firstY1;
    size = size1;
    n = n1;
    subpaths = static_cast<GfxSubpath **>(gmallocn(size, sizeof(GfxSubpath *)));
    for (i = 0; i < n; ++i) {
        subpaths[i] = subpaths1[i]->copy();
    }
}

void GfxPath::moveTo(double x, double y)
{
    justMoved = true;
    firstX = x;
    firstY = y;
}

void GfxPath::lineTo(double x, double y)
{
    if (justMoved || (n > 0 && subpaths[n - 1]->isClosed())) {
        if (n >= size) {
            size *= 2;
            subpaths = static_cast<GfxSubpath **>(greallocn(static_cast<void *>(subpaths), size, sizeof(GfxSubpath *)));
        }
        if (justMoved) {
            subpaths[n] = new GfxSubpath(firstX, firstY);
        } else {
            subpaths[n] = new GfxSubpath(subpaths[n - 1]->getLastX(), subpaths[n - 1]->getLastY());
        }
        ++n;
        justMoved = false;
    }
    subpaths[n - 1]->lineTo(x, y);
}

void GfxPath::curveTo(double x1, double y1, double x2, double y2, double x3, double y3)
{
    if (justMoved || (n > 0 && subpaths[n - 1]->isClosed())) {
        if (n >= size) {
            size *= 2;
            subpaths = static_cast<GfxSubpath **>(greallocn(static_cast<void *>(subpaths), size, sizeof(GfxSubpath *)));
        }
        if (justMoved) {
            subpaths[n] = new GfxSubpath(firstX, firstY);
        } else {
            subpaths[n] = new GfxSubpath(subpaths[n - 1]->getLastX(), subpaths[n - 1]->getLastY());
        }
        ++n;
        justMoved = false;
    }
    subpaths[n - 1]->curveTo(x1, y1, x2, y2, x3, y3);
}

void GfxPath::close()
{
    // this is necessary to handle the pathological case of
    // moveto/closepath/clip, which defines an empty clipping region
    if (justMoved) {
        if (n >= size) {
            size *= 2;
            subpaths = static_cast<GfxSubpath **>(greallocn(static_cast<void *>(subpaths), size, sizeof(GfxSubpath *)));
        }
        subpaths[n] = new GfxSubpath(firstX, firstY);
        ++n;
        justMoved = false;
    }
    subpaths[n - 1]->close();
}

void GfxPath::append(GfxPath *path)
{
    int i;

    if (n + path->n > size) {
        size = n + path->n;
        subpaths = static_cast<GfxSubpath **>(greallocn(static_cast<void *>(subpaths), size, sizeof(GfxSubpath *)));
    }
    for (i = 0; i < path->n; ++i) {
        subpaths[n++] = path->subpaths[i]->copy();
    }
    justMoved = false;
}

void GfxPath::offset(double dx, double dy)
{
    int i;

    for (i = 0; i < n; ++i) {
        subpaths[i]->offset(dx, dy);
    }
}

//------------------------------------------------------------------------
//
//------------------------------------------------------------------------

#if USE_CMS

GfxLCMSProfilePtr GfxXYZ2DisplayTransforms::XYZProfile = nullptr;

GfxXYZ2DisplayTransforms::GfxXYZ2DisplayTransforms(const GfxLCMSProfilePtr &displayProfileA)
{
    if (!XYZProfile) {
        // This is probably the one of the first invocations of lcms2, so we set the error handler
        setCMSErrorHandler();

        XYZProfile = make_GfxLCMSProfilePtr(cmsCreateXYZProfile());
    }

    displayProfile = displayProfileA;
    if (displayProfile) {
        cmsHTRANSFORM transform;
        unsigned int nChannels;
        unsigned int localDisplayPixelType;

        localDisplayPixelType = getCMSColorSpaceType(cmsGetColorSpace(displayProfile.get()));
        nChannels = getCMSNChannels(cmsGetColorSpace(displayProfile.get()));
        // create transform from XYZ
        if ((transform = cmsCreateTransform(XYZProfile.get(), TYPE_XYZ_DBL, displayProfile.get(), COLORSPACE_SH(localDisplayPixelType) | CHANNELS_SH(nChannels) | BYTES_SH(1), INTENT_RELATIVE_COLORIMETRIC, LCMS_FLAGS)) == nullptr) {
            error(errSyntaxWarning, -1, "Can't create Lab transform");
        } else {
            XYZ2DisplayTransformRelCol = std::make_shared<GfxColorTransform>(transform, INTENT_RELATIVE_COLORIMETRIC, PT_XYZ, localDisplayPixelType);
        }

        if ((transform = cmsCreateTransform(XYZProfile.get(), TYPE_XYZ_DBL, displayProfile.get(), COLORSPACE_SH(localDisplayPixelType) | CHANNELS_SH(nChannels) | BYTES_SH(1), INTENT_ABSOLUTE_COLORIMETRIC, LCMS_FLAGS)) == nullptr) {
            error(errSyntaxWarning, -1, "Can't create Lab transform");
        } else {
            XYZ2DisplayTransformAbsCol = std::make_shared<GfxColorTransform>(transform, INTENT_ABSOLUTE_COLORIMETRIC, PT_XYZ, localDisplayPixelType);
        }

        if ((transform = cmsCreateTransform(XYZProfile.get(), TYPE_XYZ_DBL, displayProfile.get(), COLORSPACE_SH(localDisplayPixelType) | CHANNELS_SH(nChannels) | BYTES_SH(1), INTENT_SATURATION, LCMS_FLAGS)) == nullptr) {
            error(errSyntaxWarning, -1, "Can't create Lab transform");
        } else {
            XYZ2DisplayTransformSat = std::make_shared<GfxColorTransform>(transform, INTENT_SATURATION, PT_XYZ, localDisplayPixelType);
        }

        if ((transform = cmsCreateTransform(XYZProfile.get(), TYPE_XYZ_DBL, displayProfile.get(), COLORSPACE_SH(localDisplayPixelType) | CHANNELS_SH(nChannels) | BYTES_SH(1), INTENT_PERCEPTUAL, LCMS_FLAGS)) == nullptr) {
            error(errSyntaxWarning, -1, "Can't create Lab transform");
        } else {
            XYZ2DisplayTransformPerc = std::make_shared<GfxColorTransform>(transform, INTENT_PERCEPTUAL, PT_XYZ, localDisplayPixelType);
        }
    } else {
        XYZ2DisplayTransformRelCol = nullptr;
        XYZ2DisplayTransformAbsCol = nullptr;
        XYZ2DisplayTransformSat = nullptr;
        XYZ2DisplayTransformPerc = nullptr;
    }
}

#endif

//------------------------------------------------------------------------
//
//------------------------------------------------------------------------
GfxState::ReusablePathIterator::ReusablePathIterator(GfxPath *pathA) : path(pathA)
{
    if (path->getNumSubpaths()) {
        curSubPath = path->getSubpath(subPathOff);
        numCoords = curSubPath->getNumPoints();
    }
}

bool GfxState::ReusablePathIterator::isEnd() const
{
    return coordOff >= numCoords;
}

void GfxState::ReusablePathIterator::next()
{
    ++coordOff;
    if (coordOff == numCoords) {
        ++subPathOff;
        if (subPathOff < path->getNumSubpaths()) {
            coordOff = 0;
            curSubPath = path->getSubpath(subPathOff);
            numCoords = curSubPath->getNumPoints();
        }
    }
}

void GfxState::ReusablePathIterator::setCoord(double x, double y)
{
    curSubPath->setX(coordOff, x);
    curSubPath->setY(coordOff, y);
}

void GfxState::ReusablePathIterator::reset()
{
    coordOff = 0;
    subPathOff = 0;
    curSubPath = path->getSubpath(0);
    numCoords = curSubPath->getNumPoints();
}

GfxState::GfxState(double hDPIA, double vDPIA, const PDFRectangle &pageBox, int rotateA, bool upsideDown)
{
    double kx, ky;

    hDPI = hDPIA;
    vDPI = vDPIA;
    rotate = rotateA;
    px1 = pageBox.x1;
    py1 = pageBox.y1;
    px2 = pageBox.x2;
    py2 = pageBox.y2;
    kx = hDPI / 72.0;
    ky = vDPI / 72.0;
    if (rotate == 90) {
        ctm[0] = 0;
        ctm[1] = upsideDown ? ky : -ky;
        ctm[2] = kx;
        ctm[3] = 0;
        ctm[4] = -kx * py1;
        ctm[5] = ky * (upsideDown ? -px1 : px2);
        pageWidth = kx * (py2 - py1);
        pageHeight = ky * (px2 - px1);
    } else if (rotate == 180) {
        ctm[0] = -kx;
        ctm[1] = 0;
        ctm[2] = 0;
        ctm[3] = upsideDown ? ky : -ky;
        ctm[4] = kx * px2;
        ctm[5] = ky * (upsideDown ? -py1 : py2);
        pageWidth = kx * (px2 - px1);
        pageHeight = ky * (py2 - py1);
    } else if (rotate == 270) {
        ctm[0] = 0;
        ctm[1] = upsideDown ? -ky : ky;
        ctm[2] = -kx;
        ctm[3] = 0;
        ctm[4] = kx * py2;
        ctm[5] = ky * (upsideDown ? px2 : -px1);
        pageWidth = kx * (py2 - py1);
        pageHeight = ky * (px2 - px1);
    } else {
        ctm[0] = kx;
        ctm[1] = 0;
        ctm[2] = 0;
        ctm[3] = upsideDown ? -ky : ky;
        ctm[4] = -kx * px1;
        ctm[5] = ky * (upsideDown ? py2 : -py1);
        pageWidth = kx * (px2 - px1);
        pageHeight = ky * (py2 - py1);
    }

    fillColorSpace = std::make_unique<GfxDeviceGrayColorSpace>();
    strokeColorSpace = std::make_unique<GfxDeviceGrayColorSpace>();
    fillColor.c[0] = 0;
    strokeColor.c[0] = 0;
    fillPattern = nullptr;
    strokePattern = nullptr;
    blendMode = gfxBlendNormal;
    fillOpacity = 1;
    strokeOpacity = 1;
    fillOverprint = false;
    strokeOverprint = false;
    overprintMode = 0;

    lineWidth = 1;
    lineDashStart = 0;
    flatness = 1;
    lineJoin = GfxState::LineJoinMitre;
    lineCap = GfxState::LineCapButt;
    miterLimit = 10;
    strokeAdjust = false;
    alphaIsShape = false;
    textKnockout = false;

    font = nullptr;
    fontSize = 0;
    textMat[0] = 1;
    textMat[1] = 0;
    textMat[2] = 0;
    textMat[3] = 1;
    textMat[4] = 0;
    textMat[5] = 0;
    charSpace = 0;
    wordSpace = 0;
    horizScaling = 1;
    leading = 0;
    rise = 0;
    render = 0;

    path = new GfxPath();
    curX = curY = 0;
    curTextX = curTextY = 0;
    lineX = lineY = 0;

    clipXMin = 0;
    clipYMin = 0;
    clipXMax = pageWidth;
    clipYMax = pageHeight;

    renderingIntent[0] = 0;

    saved = nullptr;

    defaultGrayColorSpace = nullptr;
    defaultRGBColorSpace = nullptr;
    defaultCMYKColorSpace = nullptr;
#if USE_CMS
    localDisplayProfile = nullptr;
    XYZ2DisplayTransforms = std::make_shared<GfxXYZ2DisplayTransforms>(nullptr);

    if (!sRGBProfile) {
        // This is probably the one of the first invocations of lcms2, so we set the error handler
        setCMSErrorHandler();

        sRGBProfile = make_GfxLCMSProfilePtr(cmsCreate_sRGBProfile());
    }
#endif
}

GfxState::~GfxState()
{
    delete path;
}

// Used for copy();
GfxState::GfxState(const GfxState *state, bool copyPath)
{
    hDPI = state->hDPI;
    vDPI = state->vDPI;
    ctm = state->ctm;
    px1 = state->px1;
    py1 = state->py1;
    px2 = state->px2;
    py2 = state->py2;
    pageWidth = state->pageWidth;
    pageHeight = state->pageHeight;
    rotate = state->rotate;

    if (state->fillColorSpace) {
        fillColorSpace = state->fillColorSpace->copy();
    }
    if (state->strokeColorSpace) {
        strokeColorSpace = state->strokeColorSpace->copy();
    }
    fillColor = state->fillColor;
    strokeColor = state->strokeColor;

    if (state->fillPattern) {
        fillPattern = state->fillPattern->copy();
    }
    if (state->strokePattern) {
        strokePattern = state->strokePattern->copy();
    }
    blendMode = state->blendMode;
    fillOpacity = state->fillOpacity;
    strokeOpacity = state->strokeOpacity;
    fillOverprint = state->fillOverprint;
    strokeOverprint = state->strokeOverprint;
    overprintMode = state->overprintMode;
    transfer.reserve(state->transfer.size());
    for (const auto &element : state->transfer) {
        transfer.push_back(element->copy());
    }
    lineWidth = state->lineWidth;
    lineDash = state->lineDash;
    lineDashStart = state->lineDashStart;
    flatness = state->flatness;
    lineJoin = state->lineJoin;
    lineCap = state->lineCap;
    miterLimit = state->miterLimit;
    strokeAdjust = state->strokeAdjust;
    alphaIsShape = state->alphaIsShape;
    textKnockout = state->textKnockout;

    font = state->font;
    fontSize = state->fontSize;
    textMat = state->textMat;
    charSpace = state->charSpace;
    wordSpace = state->wordSpace;
    horizScaling = state->horizScaling;
    leading = state->leading;
    rise = state->rise;
    render = state->render;

    path = state->path;
    if (copyPath) {
        path = state->path->copy();
    }
    curX = state->curX;
    curY = state->curY;
    curTextX = state->curTextX;
    curTextY = state->curTextY;
    lineX = state->lineX;
    lineY = state->lineY;

    clipXMin = state->clipXMin;
    clipYMin = state->clipYMin;
    clipXMax = state->clipXMax;
    clipYMax = state->clipYMax;
    memcpy(renderingIntent, state->renderingIntent, sizeof(renderingIntent));

    saved = nullptr;
#if USE_CMS
    localDisplayProfile = state->localDisplayProfile;
    XYZ2DisplayTransforms = state->XYZ2DisplayTransforms;
#endif

    if (state->defaultGrayColorSpace) {
        defaultGrayColorSpace = state->defaultGrayColorSpace->copy();
    } else {
        defaultGrayColorSpace = nullptr;
    }
    if (state->defaultRGBColorSpace) {
        defaultRGBColorSpace = state->defaultRGBColorSpace->copy();
    } else {
        defaultRGBColorSpace = nullptr;
    }
    if (state->defaultCMYKColorSpace) {
        defaultCMYKColorSpace = state->defaultCMYKColorSpace->copy();
    } else {
        defaultCMYKColorSpace = nullptr;
    }
}

#if USE_CMS

GfxLCMSProfilePtr GfxState::sRGBProfile = nullptr;

void GfxState::setDisplayProfile(const GfxLCMSProfilePtr &localDisplayProfileA)
{
    localDisplayProfile = localDisplayProfileA;
    XYZ2DisplayTransforms = std::make_shared<GfxXYZ2DisplayTransforms>(localDisplayProfile);
}

void GfxState::setXYZ2DisplayTransforms(std::shared_ptr<GfxXYZ2DisplayTransforms> transforms)
{
    XYZ2DisplayTransforms = std::move(transforms);
    localDisplayProfile = XYZ2DisplayTransforms->getDisplayProfile();
}

std::shared_ptr<GfxColorTransform> GfxState::getXYZ2DisplayTransform()
{
    auto transform = XYZ2DisplayTransforms->getRelCol();
    if (strcmp(renderingIntent, "AbsoluteColorimetric") == 0) {
        transform = XYZ2DisplayTransforms->getAbsCol();
    } else if (strcmp(renderingIntent, "Saturation") == 0) {
        transform = XYZ2DisplayTransforms->getSat();
    } else if (strcmp(renderingIntent, "Perceptual") == 0) {
        transform = XYZ2DisplayTransforms->getPerc();
    }
    return transform;
}

int GfxState::getCmsRenderingIntent() const
{
    const char *intent = getRenderingIntent();
    int cmsIntent = INTENT_RELATIVE_COLORIMETRIC;
    if (intent) {
        if (strcmp(intent, "AbsoluteColorimetric") == 0) {
            cmsIntent = INTENT_ABSOLUTE_COLORIMETRIC;
        } else if (strcmp(intent, "Saturation") == 0) {
            cmsIntent = INTENT_SATURATION;
        } else if (strcmp(intent, "Perceptual") == 0) {
            cmsIntent = INTENT_PERCEPTUAL;
        }
    }
    return cmsIntent;
}

#endif

void GfxState::getUserClipBBox(double *xMin, double *yMin, double *xMax, double *yMax) const
{
    double ictm[6];
    double xMin1, yMin1, xMax1, yMax1, tx, ty;

    // invert the CTM
    const double det_denominator = (ctm[0] * ctm[3] - ctm[1] * ctm[2]);
    if (unlikely(det_denominator == 0)) {
        *xMin = 0;
        *yMin = 0;
        *xMax = 0;
        *yMax = 0;
        return;
    }
    const double det = 1 / det_denominator;
    ictm[0] = ctm[3] * det;
    ictm[1] = -ctm[1] * det;
    ictm[2] = -ctm[2] * det;
    ictm[3] = ctm[0] * det;
    ictm[4] = (ctm[2] * ctm[5] - ctm[3] * ctm[4]) * det;
    ictm[5] = (ctm[1] * ctm[4] - ctm[0] * ctm[5]) * det;

    // transform all four corners of the clip bbox; find the min and max
    // x and y values
    xMin1 = xMax1 = clipXMin * ictm[0] + clipYMin * ictm[2] + ictm[4];
    yMin1 = yMax1 = clipXMin * ictm[1] + clipYMin * ictm[3] + ictm[5];
    tx = clipXMin * ictm[0] + clipYMax * ictm[2] + ictm[4];
    ty = clipXMin * ictm[1] + clipYMax * ictm[3] + ictm[5];
    if (tx < xMin1) {
        xMin1 = tx;
    } else if (tx > xMax1) {
        xMax1 = tx;
    }
    if (ty < yMin1) {
        yMin1 = ty;
    } else if (ty > yMax1) {
        yMax1 = ty;
    }
    tx = clipXMax * ictm[0] + clipYMin * ictm[2] + ictm[4];
    ty = clipXMax * ictm[1] + clipYMin * ictm[3] + ictm[5];
    if (tx < xMin1) {
        xMin1 = tx;
    } else if (tx > xMax1) {
        xMax1 = tx;
    }
    if (ty < yMin1) {
        yMin1 = ty;
    } else if (ty > yMax1) {
        yMax1 = ty;
    }
    tx = clipXMax * ictm[0] + clipYMax * ictm[2] + ictm[4];
    ty = clipXMax * ictm[1] + clipYMax * ictm[3] + ictm[5];
    if (tx < xMin1) {
        xMin1 = tx;
    } else if (tx > xMax1) {
        xMax1 = tx;
    }
    if (ty < yMin1) {
        yMin1 = ty;
    } else if (ty > yMax1) {
        yMax1 = ty;
    }

    *xMin = xMin1;
    *yMin = yMin1;
    *xMax = xMax1;
    *yMax = yMax1;
}

double GfxState::transformWidth(double w) const
{
    double x, y;

    x = ctm[0] + ctm[2];
    y = ctm[1] + ctm[3];
    return w * sqrt(0.5 * (x * x + y * y));
}

double GfxState::getTransformedFontSize() const
{
    double x1, y1, x2, y2;

    x1 = textMat[2] * fontSize;
    y1 = textMat[3] * fontSize;
    x2 = ctm[0] * x1 + ctm[2] * y1;
    y2 = ctm[1] * x1 + ctm[3] * y1;
    return sqrt(x2 * x2 + y2 * y2);
}

void GfxState::getFontTransMat(double *m11, double *m12, double *m21, double *m22) const
{
    *m11 = (textMat[0] * ctm[0] + textMat[1] * ctm[2]) * fontSize;
    *m12 = (textMat[0] * ctm[1] + textMat[1] * ctm[3]) * fontSize;
    *m21 = (textMat[2] * ctm[0] + textMat[3] * ctm[2]) * fontSize;
    *m22 = (textMat[2] * ctm[1] + textMat[3] * ctm[3]) * fontSize;
}

void GfxState::setCTM(double a, double b, double c, double d, double e, double f)
{
    ctm[0] = a;
    ctm[1] = b;
    ctm[2] = c;
    ctm[3] = d;
    ctm[4] = e;
    ctm[5] = f;
}

void GfxState::concatCTM(double a, double b, double c, double d, double e, double f)
{
    double a1 = ctm[0];
    double b1 = ctm[1];
    double c1 = ctm[2];
    double d1 = ctm[3];

    ctm[0] = a * a1 + b * c1;
    ctm[1] = a * b1 + b * d1;
    ctm[2] = c * a1 + d * c1;
    ctm[3] = c * b1 + d * d1;
    ctm[4] = e * a1 + f * c1 + ctm[4];
    ctm[5] = e * b1 + f * d1 + ctm[5];
}

void GfxState::shiftCTMAndClip(double tx, double ty)
{
    ctm[4] += tx;
    ctm[5] += ty;
    clipXMin += tx;
    clipYMin += ty;
    clipXMax += tx;
    clipYMax += ty;
}

void GfxState::setFillColorSpace(std::unique_ptr<GfxColorSpace> &&colorSpace)
{
    fillColorSpace = std::move(colorSpace);
}

void GfxState::setStrokeColorSpace(std::unique_ptr<GfxColorSpace> &&colorSpace)
{
    strokeColorSpace = std::move(colorSpace);
}

void GfxState::setFillPattern(std::unique_ptr<GfxPattern> &&pattern)
{
    fillPattern = std::move(pattern);
}

void GfxState::setStrokePattern(std::unique_ptr<GfxPattern> &&pattern)
{
    strokePattern = std::move(pattern);
}

void GfxState::setFont(std::shared_ptr<GfxFont> fontA, double fontSizeA)
{
    font = std::move(fontA);
    fontSize = fontSizeA;
}

void GfxState::setTransfer(std::vector<std::unique_ptr<Function>> funcs)
{
    transfer = std::move(funcs);
}

void GfxState::setLineDash(std::vector<double> &&dash, double start)
{
    lineDash = dash;
    lineDashStart = start;
}

void GfxState::clearPath()
{
    delete path;
    path = new GfxPath();
}

void GfxState::clip()
{
    double xMin, yMin, xMax, yMax, x, y;
    GfxSubpath *subpath;
    int i, j;

    xMin = xMax = yMin = yMax = 0; // make gcc happy
    for (i = 0; i < path->getNumSubpaths(); ++i) {
        subpath = path->getSubpath(i);
        for (j = 0; j < subpath->getNumPoints(); ++j) {
            transform(subpath->getX(j), subpath->getY(j), &x, &y);
            if (i == 0 && j == 0) {
                xMin = xMax = x;
                yMin = yMax = y;
            } else {
                if (x < xMin) {
                    xMin = x;
                } else if (x > xMax) {
                    xMax = x;
                }
                if (y < yMin) {
                    yMin = y;
                } else if (y > yMax) {
                    yMax = y;
                }
            }
        }
    }
    if (xMin > clipXMin) {
        clipXMin = xMin;
    }
    if (yMin > clipYMin) {
        clipYMin = yMin;
    }
    if (xMax < clipXMax) {
        clipXMax = xMax;
    }
    if (yMax < clipYMax) {
        clipYMax = yMax;
    }
}

void GfxState::clipToStrokePath()
{
    double xMin, yMin, xMax, yMax, x, y, t0, t1;
    GfxSubpath *subpath;
    int i, j;

    xMin = xMax = yMin = yMax = 0; // make gcc happy
    for (i = 0; i < path->getNumSubpaths(); ++i) {
        subpath = path->getSubpath(i);
        for (j = 0; j < subpath->getNumPoints(); ++j) {
            transform(subpath->getX(j), subpath->getY(j), &x, &y);
            if (i == 0 && j == 0) {
                xMin = xMax = x;
                yMin = yMax = y;
            } else {
                if (x < xMin) {
                    xMin = x;
                } else if (x > xMax) {
                    xMax = x;
                }
                if (y < yMin) {
                    yMin = y;
                } else if (y > yMax) {
                    yMax = y;
                }
            }
        }
    }

    // allow for the line width
    //~ miter joins can extend farther than this
    t0 = fabs(ctm[0]);
    t1 = fabs(ctm[2]);
    if (t0 > t1) {
        xMin -= 0.5 * lineWidth * t0;
        xMax += 0.5 * lineWidth * t0;
    } else {
        xMin -= 0.5 * lineWidth * t1;
        xMax += 0.5 * lineWidth * t1;
    }
    t0 = fabs(ctm[0]);
    t1 = fabs(ctm[3]);
    if (t0 > t1) {
        yMin -= 0.5 * lineWidth * t0;
        yMax += 0.5 * lineWidth * t0;
    } else {
        yMin -= 0.5 * lineWidth * t1;
        yMax += 0.5 * lineWidth * t1;
    }

    if (xMin > clipXMin) {
        clipXMin = xMin;
    }
    if (yMin > clipYMin) {
        clipYMin = yMin;
    }
    if (xMax < clipXMax) {
        clipXMax = xMax;
    }
    if (yMax < clipYMax) {
        clipYMax = yMax;
    }
}

void GfxState::clipToRect(double xMin, double yMin, double xMax, double yMax)
{
    double x, y, xMin1, yMin1, xMax1, yMax1;

    transform(xMin, yMin, &x, &y);
    xMin1 = xMax1 = x;
    yMin1 = yMax1 = y;
    transform(xMax, yMin, &x, &y);
    if (x < xMin1) {
        xMin1 = x;
    } else if (x > xMax1) {
        xMax1 = x;
    }
    if (y < yMin1) {
        yMin1 = y;
    } else if (y > yMax1) {
        yMax1 = y;
    }
    transform(xMax, yMax, &x, &y);
    if (x < xMin1) {
        xMin1 = x;
    } else if (x > xMax1) {
        xMax1 = x;
    }
    if (y < yMin1) {
        yMin1 = y;
    } else if (y > yMax1) {
        yMax1 = y;
    }
    transform(xMin, yMax, &x, &y);
    if (x < xMin1) {
        xMin1 = x;
    } else if (x > xMax1) {
        xMax1 = x;
    }
    if (y < yMin1) {
        yMin1 = y;
    } else if (y > yMax1) {
        yMax1 = y;
    }

    if (xMin1 > clipXMin) {
        clipXMin = xMin1;
    }
    if (yMin1 > clipYMin) {
        clipYMin = yMin1;
    }
    if (xMax1 < clipXMax) {
        clipXMax = xMax1;
    }
    if (yMax1 < clipYMax) {
        clipYMax = yMax1;
    }
}

void GfxState::textShift(double tx, double ty)
{
    double dx, dy;

    textTransformDelta(tx, ty, &dx, &dy);
    curTextX += dx;
    curTextY += dy;
}

void GfxState::textShiftWithUserCoords(double dx, double dy)
{
    curTextX += dx;
    curTextY += dy;
}

GfxState *GfxState::save()
{
    GfxState *newState;

    newState = copy();
    newState->saved = this;
    return newState;
}

GfxState *GfxState::restore()
{
    GfxState *oldState;

    if (saved) {
        oldState = saved;

        // these attributes aren't saved/restored by the q/Q operators
        oldState->path = path;
        oldState->curX = curX;
        oldState->curY = curY;
        oldState->curTextX = curTextX;
        oldState->curTextY = curTextY;
        oldState->lineX = lineX;
        oldState->lineY = lineY;

        path = nullptr;
        saved = nullptr;
        delete this;

    } else {
        oldState = this;
    }

    return oldState;
}

bool GfxState::parseBlendMode(Object *obj, GfxBlendMode *mode)
{
    int i, j;

    if (obj->isName()) {
        for (i = 0; i < nGfxBlendModeNames; ++i) {
            if (obj->getNameString() == gfxBlendModeNames[i].name) {
                *mode = gfxBlendModeNames[i].mode;
                return true;
            }
        }
        return false;
    }
    if (obj->isArray()) {
        for (i = 0; i < obj->arrayGetLength(); ++i) {
            Object obj2 = obj->arrayGet(i);
            if (!obj2.isName()) {
                return false;
            }
            for (j = 0; j < nGfxBlendModeNames; ++j) {
                if (obj2.getNameString() == gfxBlendModeNames[j].name) {
                    *mode = gfxBlendModeNames[j].mode;
                    return true;
                }
            }
        }
        *mode = gfxBlendNormal;
        return true;
    }
    return false;
}
