//========================================================================
//
// SplashOutputDev.cc
//
// Copyright 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 Takashi Iwai <tiwai@suse.de>
// Copyright (C) 2006 Stefan Schweizer <genstef@gentoo.org>
// Copyright (C) 2006-2022, 2024-2026 Albert Astals Cid <aacid@kde.org>
// Copyright (C) 2006 Krzysztof Kowalczyk <kkowalczyk@gmail.com>
// Copyright (C) 2006 Scott Turner <scotty1024@mac.com>
// Copyright (C) 2007 Koji Otani <sho@bbr.jp>
// Copyright (C) 2009 Petr Gajdos <pgajdos@novell.com>
// Copyright (C) 2009-2016, 2020, 2022, 2023 Thomas Freitag <Thomas.Freitag@alfa.de>
// Copyright (C) 2009 Carlos Garcia Campos <carlosgc@gnome.org>
// Copyright (C) 2009, 2014-2016, 2019 William Bader <williambader@hotmail.com>
// Copyright (C) 2010 Patrick Spendrin <ps_ml@gmx.de>
// Copyright (C) 2010 Brian Cameron <brian.cameron@oracle.com>
// Copyright (C) 2010 Paweł Wiejacha <pawel.wiejacha@gmail.com>
// Copyright (C) 2010 Christian Feuersänger <cfeuersaenger@googlemail.com>
// Copyright (C) 2011 Andreas Hartmetz <ahartmetz@gmail.com>
// Copyright (C) 2011 Andrea Canciani <ranma42@gmail.com>
// Copyright (C) 2011, 2012, 2017 Adrian Johnson <ajohnson@redneon.com>
// Copyright (C) 2013 Lu Wang <coolwanglu@gmail.com>
// Copyright (C) 2013 Li Junling <lijunling@sina.com>
// Copyright (C) 2014 Ed Porras <ed@moto-research.com>
// Copyright (C) 2014 Richard PALO <richard@netbsd.org>
// Copyright (C) 2015 Tamas Szekeres <szekerest@gmail.com>
// Copyright (C) 2015 Kenji Uno <ku@digitaldolphins.jp>
// Copyright (C) 2016 Takahiro Hashimoto <kenya888.en@gmail.com>
// Copyright (C) 2017, 2021, 2024 Even Rouault <even.rouault@spatialys.com>
// 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, 2019 Stefan Brüns <stefan.bruens@rwth-aachen.de>
// Copyright (C) 2018 Adam Reichold <adam.reichold@t-online.de>
// Copyright (C) 2019 Christian Persch <chpe@src.gnome.org>
// Copyright (C) 2020-2022 Oliver Sander <oliver.sander@tu-dresden.de>
// Copyright (C) 2024-2026 g10 Code GmbH, Author: Sune Stolborg Vuorela <sune@vuorela.dk>
// Copyright (C) 2025 Arnav V <arnav0872@gmail.com>
// Copyright (C) 2026 Taufeeque Sifat <entity069@protonmail.com>
// Copyright (C) 2026 Marek Kasik <mkasik@redhat.com>
//
// 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 <climits>
#include <cstring>
#include <cmath>
#include <vector>
#include "Stream.h"
#include "GlobalParams.h"
#include "Error.h"
#include "Object.h"
#include "Gfx.h"
#include "GfxFont.h"
#include "Page.h"
#include "PDFDoc.h"
#include "Link.h"
#include "fofi/FoFiTrueType.h"
#include "goo/gmem.h"
#include "splash/SplashBitmap.h"
#include "splash/SplashGlyphBitmap.h"
#include "splash/SplashPattern.h"
#include "splash/SplashPath.h"
#include "splash/SplashState.h"
#include "splash/SplashErrorCodes.h"
#include "splash/SplashFontEngine.h"
#include "splash/SplashFont.h"
#include "splash/SplashFontFile.h"
#include "splash/SplashFontFileID.h"
#include "splash/SplashMath.h"
#include "splash/Splash.h"
#include "SplashOutputDev.h"
#include <algorithm>

static const double s_minLineWidth = 0.0;

static inline void convertGfxColor(SplashColorPtr dest, const SplashColorMode colorMode, const GfxColorSpace *colorSpace, const GfxColor &src)
{
    switch (colorMode) {
    case splashModeMono1:
    case splashModeMono8: {
        GfxGray gray;
        colorSpace->getGray(src, &gray);
        dest[0] = colToByte(gray);
        memset(&dest[1], 0, splashMaxColorComps - 1);
        break;
    }
    case splashModeXBGR8:
    case splashModeBGR8:
    case splashModeRGB8: {
        GfxRGB rgb;
        colorSpace->getRGB(src, &rgb);
        dest[0] = colToByte(rgb.r);
        dest[1] = colToByte(rgb.g);
        dest[2] = colToByte(rgb.b);
        dest[3] = colorMode == splashModeXBGR8 ? 255 : 0;
        memset(&dest[4], 0, splashMaxColorComps - 4);
        break;
    }
    case splashModeCMYK8: {
        GfxCMYK cmyk;
        colorSpace->getCMYK(src, &cmyk);
        dest[0] = colToByte(cmyk.c);
        dest[1] = colToByte(cmyk.m);
        dest[2] = colToByte(cmyk.y);
        dest[3] = colToByte(cmyk.k);
        memset(&dest[4], 0, splashMaxColorComps - 4);
        break;
    }
    case splashModeDeviceN8: {
        GfxColor deviceN;
        colorSpace->getDeviceN(src, &deviceN);
        for (int i = 0; i < SPOT_NCOMPS + 4; i++) {
            dest[i] = colToByte(deviceN.c[i]);
        }
        break;
    }
    }
}

// Copy a color according to the color mode.
// Use convertGfxShortColor() below when the destination is a bitmap
// to avoid overwriting cells.
// Calling this in SplashGouraudPattern::getParameterizedColor() fixes bug 90570.
// Use convertGfxColor() above when the destination is an array of SPOT_NCOMPS+4 bytes,
// to ensure that everything is initialized.

static inline void convertGfxShortColor(SplashColorPtr dest, const SplashColorMode colorMode, const GfxColorSpace *colorSpace, const GfxColor &src)
{
    switch (colorMode) {
    case splashModeMono1:
    case splashModeMono8: {
        GfxGray gray;
        colorSpace->getGray(src, &gray);
        dest[0] = colToByte(gray);
    } break;
    case splashModeXBGR8:
        dest[3] = 255;
        // fallthrough
    case splashModeBGR8:
    case splashModeRGB8: {
        GfxRGB rgb;
        colorSpace->getRGB(src, &rgb);
        dest[0] = colToByte(rgb.r);
        dest[1] = colToByte(rgb.g);
        dest[2] = colToByte(rgb.b);
    } break;
    case splashModeCMYK8: {
        GfxCMYK cmyk;
        colorSpace->getCMYK(src, &cmyk);
        dest[0] = colToByte(cmyk.c);
        dest[1] = colToByte(cmyk.m);
        dest[2] = colToByte(cmyk.y);
        dest[3] = colToByte(cmyk.k);
    } break;
    case splashModeDeviceN8: {
        GfxColor deviceN;
        colorSpace->getDeviceN(src, &deviceN);
        for (int i = 0; i < SPOT_NCOMPS + 4; i++) {
            dest[i] = colToByte(deviceN.c[i]);
        }
    } break;
    }
}

//------------------------------------------------------------------------
// SplashGouraudPattern
//------------------------------------------------------------------------
SplashGouraudPattern::SplashGouraudPattern(bool bDirectColorTranslationA, GfxGouraudTriangleShading *shadingA)
{
    shading = shadingA;
    bDirectColorTranslation = bDirectColorTranslationA;
    gfxMode = shadingA->getColorSpace()->getMode();
}

SplashGouraudPattern::~SplashGouraudPattern() = default;

void SplashGouraudPattern::getNonParametrizedTriangle(int i, SplashColorMode mode, double *x0, double *y0, SplashColorPtr color0, double *x1, double *y1, SplashColorPtr color1, double *x2, double *y2, SplashColorPtr color2)
{
    GfxColor c0, c1, c2;
    shading->getTriangle(i, x0, y0, &c0, x1, y1, &c1, x2, y2, &c2);

    const GfxColorSpace *srcColorSpace = shading->getColorSpace();
    convertGfxColor(color0, mode, srcColorSpace, c0);
    convertGfxColor(color1, mode, srcColorSpace, c1);
    convertGfxColor(color2, mode, srcColorSpace, c2);
}

void SplashGouraudPattern::getParameterizedColor(double colorinterp, SplashColorMode mode, SplashColorPtr dest)
{
    GfxColor src;
    shading->getParameterizedColor(colorinterp, &src);

    if (bDirectColorTranslation) {
        const int colorComps = splashColorModeNComps[mode];
        for (int m = 0; m < colorComps; ++m) {
            dest[m] = colToByte(src.c[m]);
        }
    } else {
        GfxColorSpace *srcColorSpace = shading->getColorSpace();
        convertGfxShortColor(dest, mode, srcColorSpace, src);
    }
}

//------------------------------------------------------------------------
// SplashFunctionPattern
//------------------------------------------------------------------------

SplashFunctionPattern::SplashFunctionPattern(SplashColorMode colorModeA, GfxState *stateA, GfxFunctionShading *shadingA)
{
    Matrix ctm;
    SplashColor defaultColor;
    GfxColor srcColor;
    const std::array<double, 6> &matrix = shadingA->getMatrix();

    shading = shadingA;
    state = stateA;
    colorMode = colorModeA;

    state->getCTM(&ctm);

    double a1 = ctm.m[0];
    double b1 = ctm.m[1];
    double c1 = ctm.m[2];
    double d1 = ctm.m[3];

    ctm.m[0] = matrix[0] * a1 + matrix[1] * c1;
    ctm.m[1] = matrix[0] * b1 + matrix[1] * d1;
    ctm.m[2] = matrix[2] * a1 + matrix[3] * c1;
    ctm.m[3] = matrix[2] * b1 + matrix[3] * d1;
    ctm.m[4] = matrix[4] * a1 + matrix[5] * c1 + ctm.m[4];
    ctm.m[5] = matrix[4] * b1 + matrix[5] * d1 + ctm.m[5];
    ctm.invertTo(&ictm);

    gfxMode = shadingA->getColorSpace()->getMode();
    shadingA->getColorSpace()->getDefaultColor(&srcColor);
    shadingA->getDomain(&xMin, &yMin, &xMax, &yMax);
    convertGfxColor(defaultColor, colorModeA, shadingA->getColorSpace(), srcColor);
}

SplashFunctionPattern::~SplashFunctionPattern() = default;

bool SplashFunctionPattern::getColor(int x, int y, SplashColorPtr c) const
{
    GfxColor gfxColor;
    double xc, yc;

    ictm.transform(x, y, &xc, &yc);
    if (xc < xMin || xc > xMax || yc < yMin || yc > yMax) {
        return false;
    }
    shading->getColor(xc, yc, &gfxColor);
    convertGfxColor(c, colorMode, shading->getColorSpace(), gfxColor);
    return true;
}

//------------------------------------------------------------------------
// SplashUnivariatePattern
//------------------------------------------------------------------------

SplashUnivariatePattern::SplashUnivariatePattern(SplashColorMode colorModeA, GfxState *stateA, GfxUnivariateShading *shadingA)
{
    Matrix ctm;
    double xMin, yMin, xMax, yMax;

    shading = shadingA;
    state = stateA;
    colorMode = colorModeA;

    state->getCTM(&ctm);
    ctm.invertTo(&ictm);

    // get the function domain
    t0 = shading->getDomain0();
    t1 = shading->getDomain1();
    dt = t1 - t0;

    stateA->getUserClipBBox(&xMin, &yMin, &xMax, &yMax);
    shadingA->setupCache(&ctm, xMin, yMin, xMax, yMax);
    gfxMode = shadingA->getColorSpace()->getMode();
}

SplashUnivariatePattern::~SplashUnivariatePattern() = default;

bool SplashUnivariatePattern::getColor(int x, int y, SplashColorPtr c) const
{
    GfxColor gfxColor;
    double xc, yc, t;

    ictm.transform(x, y, &xc, &yc);
    if (!getParameter(xc, yc, &t)) {
        return false;
    }

    const int filled = shading->getColor(t, &gfxColor);
    if (unlikely(filled < shading->getColorSpace()->getNComps())) {
        for (int i = filled; i < shading->getColorSpace()->getNComps(); ++i) {
            gfxColor.c[i] = 0;
        }
    }
    convertGfxColor(c, colorMode, shading->getColorSpace(), gfxColor);
    return true;
}

bool SplashUnivariatePattern::testPosition(int x, int y) const
{
    double xc, yc, t;

    ictm.transform(x, y, &xc, &yc);
    if (!getParameter(xc, yc, &t)) {
        return false;
    }
    return (t0 < t1) ? (t > t0 && t < t1) : (t > t1 && t < t0);
}

//------------------------------------------------------------------------
// SplashRadialPattern
//------------------------------------------------------------------------
#define RADIAL_EPSILON (1. / 1024 / 1024)

SplashRadialPattern::SplashRadialPattern(SplashColorMode colorModeA, GfxState *stateA, GfxRadialShading *shadingA) : SplashUnivariatePattern(colorModeA, stateA, shadingA)
{
    SplashColor defaultColor;
    GfxColor srcColor;

    shadingA->getCoords(&x0, &y0, &r0, &dx, &dy, &dr);
    dx -= x0;
    dy -= y0;
    dr -= r0;
    a = dx * dx + dy * dy - dr * dr;
    if (fabs(a) > RADIAL_EPSILON) {
        inva = 1.0 / a;
    }
    shadingA->getColorSpace()->getDefaultColor(&srcColor);
    convertGfxColor(defaultColor, colorModeA, shadingA->getColorSpace(), srcColor);
}

SplashRadialPattern::~SplashRadialPattern() = default;

bool SplashRadialPattern::getParameter(double xs, double ys, double *t) const
{
    double b, c, s0, s1;

    // We want to solve this system of equations:
    //
    // 1. (x - xc(s))^2 + (y -yc(s))^2 = rc(s)^2
    // 2. xc(s) = x0 + s * (x1 - xo)
    // 3. yc(s) = y0 + s * (y1 - yo)
    // 4. rc(s) = r0 + s * (r1 - ro)
    //
    // To simplify the system a little, we translate
    // our coordinates to have the origin in (x0,y0)

    xs -= x0;
    ys -= y0;

    // Then we have to solve the equation:
    //   A*s^2 - 2*B*s + C = 0
    // where
    //   A = dx^2  + dy^2  - dr^2
    //   B = xs*dx + ys*dy + r0*dr
    //   C = xs^2  + ys^2  - r0^2

    b = xs * dx + ys * dy + r0 * dr;
    c = xs * xs + ys * ys - r0 * r0;

    if (fabs(a) <= RADIAL_EPSILON) {
        // A is 0, thus the equation simplifies to:
        //   -2*B*s + C = 0
        // If B is 0, we can either have no solution or an indeterminate
        // equation, thus we behave as if we had an invalid solution
        if (fabs(b) <= RADIAL_EPSILON) {
            return false;
        }

        s0 = s1 = 0.5 * c / b;
    } else {
        double d;

        d = b * b - a * c;
        if (d < 0) {
            return false;
        }

        d = sqrt(d);
        s0 = b + d;
        s1 = b - d;

        // If A < 0, one of the two solutions will have negative radius,
        // thus it will be ignored. Otherwise we know that s1 <= s0
        // (because d >=0 implies b - d <= b + d), so if both are valid it
        // will be the true solution.
        s0 *= inva;
        s1 *= inva;
    }

    if (r0 + s0 * dr >= 0) {
        if (0 <= s0 && s0 <= 1) {
            *t = t0 + dt * s0;
            return true;
        }
        if (s0 < 0 && shading->getExtend0()) {
            *t = t0;
            return true;
        }
        if (s0 > 1 && shading->getExtend1()) {
            *t = t1;
            return true;
        }
    }

    if (r0 + s1 * dr >= 0) {
        if (0 <= s1 && s1 <= 1) {
            *t = t0 + dt * s1;
            return true;
        }
        if (s1 < 0 && shading->getExtend0()) {
            *t = t0;
            return true;
        }
        if (s1 > 1 && shading->getExtend1()) {
            *t = t1;
            return true;
        }
    }

    return false;
}

#undef RADIAL_EPSILON

//------------------------------------------------------------------------
// SplashAxialPattern
//------------------------------------------------------------------------

SplashAxialPattern::SplashAxialPattern(SplashColorMode colorModeA, GfxState *stateA, GfxAxialShading *shadingA) : SplashUnivariatePattern(colorModeA, stateA, shadingA)
{
    SplashColor defaultColor;
    GfxColor srcColor;

    shadingA->getCoords(&x0, &y0, &x1, &y1);
    dx = x1 - x0;
    dy = y1 - y0;
    const double mul_denominator = (dx * dx + dy * dy);
    if (unlikely(mul_denominator == 0)) {
        mul = 0;
    } else {
        mul = 1 / mul_denominator;
    }
    shadingA->getColorSpace()->getDefaultColor(&srcColor);
    convertGfxColor(defaultColor, colorModeA, shadingA->getColorSpace(), srcColor);
}

SplashAxialPattern::~SplashAxialPattern() = default;

bool SplashAxialPattern::getParameter(double xc, double yc, double *t) const
{
    double s;

    xc -= x0;
    yc -= y0;

    s = (xc * dx + yc * dy) * mul;
    if (0 <= s && s <= 1) {
        *t = t0 + dt * s;
    } else if (s < 0 && shading->getExtend0()) {
        *t = t0;
    } else if (s > 1 && shading->getExtend1()) {
        *t = t1;
    } else {
        return false;
    }

    return true;
}

//------------------------------------------------------------------------
// Type 3 font cache size parameters
constexpr int type3FontCacheAssoc = 8;
constexpr int type3FontCacheMaxSets = 8;
constexpr int type3FontCacheSize = 128 * 1024;

//------------------------------------------------------------------------
// Divide a 16-bit value (in [0, 255*255]) by 255, returning an 8-bit result.
static inline unsigned char div255(int x)
{
    return static_cast<unsigned char>((x + (x >> 8) + 0x80) >> 8);
}

//------------------------------------------------------------------------
// Blend functions
//------------------------------------------------------------------------

static void splashOutBlendMultiply(SplashColorPtr src, SplashColorPtr dest, SplashColorPtr blend, SplashColorMode cm)
{
    int i;

    if (cm == splashModeCMYK8 || cm == splashModeDeviceN8) {
        for (i = 0; i < splashColorModeNComps[cm]; ++i) {
            dest[i] = 255 - dest[i];
            src[i] = 255 - src[i];
        }
    }
    {
        for (i = 0; i < splashColorModeNComps[cm]; ++i) {
            blend[i] = (dest[i] * src[i]) / 255;
        }
    }
    if (cm == splashModeCMYK8 || cm == splashModeDeviceN8) {
        for (i = 0; i < splashColorModeNComps[cm]; ++i) {
            dest[i] = 255 - dest[i];
            src[i] = 255 - src[i];
            blend[i] = 255 - blend[i];
        }
    }
}

static void splashOutBlendScreen(SplashColorPtr src, SplashColorPtr dest, SplashColorPtr blend, SplashColorMode cm)
{
    int i;

    if (cm == splashModeCMYK8 || cm == splashModeDeviceN8) {
        for (i = 0; i < splashColorModeNComps[cm]; ++i) {
            dest[i] = 255 - dest[i];
            src[i] = 255 - src[i];
        }
    }
    {
        for (i = 0; i < splashColorModeNComps[cm]; ++i) {
            blend[i] = dest[i] + src[i] - (dest[i] * src[i]) / 255;
        }
    }
    if (cm == splashModeCMYK8 || cm == splashModeDeviceN8) {
        for (i = 0; i < splashColorModeNComps[cm]; ++i) {
            dest[i] = 255 - dest[i];
            src[i] = 255 - src[i];
            blend[i] = 255 - blend[i];
        }
    }
}

static void splashOutBlendOverlay(SplashColorPtr src, SplashColorPtr dest, SplashColorPtr blend, SplashColorMode cm)
{
    int i;

    if (cm == splashModeCMYK8 || cm == splashModeDeviceN8) {
        for (i = 0; i < splashColorModeNComps[cm]; ++i) {
            dest[i] = 255 - dest[i];
            src[i] = 255 - src[i];
        }
    }
    {
        for (i = 0; i < splashColorModeNComps[cm]; ++i) {
            blend[i] = dest[i] < 0x80 ? (src[i] * 2 * dest[i]) / 255 : 255 - 2 * ((255 - src[i]) * (255 - dest[i])) / 255;
        }
    }
    if (cm == splashModeCMYK8 || cm == splashModeDeviceN8) {
        for (i = 0; i < splashColorModeNComps[cm]; ++i) {
            dest[i] = 255 - dest[i];
            src[i] = 255 - src[i];
            blend[i] = 255 - blend[i];
        }
    }
}

static void splashOutBlendDarken(SplashColorPtr src, SplashColorPtr dest, SplashColorPtr blend, SplashColorMode cm)
{
    int i;

    if (cm == splashModeCMYK8 || cm == splashModeDeviceN8) {
        for (i = 0; i < splashColorModeNComps[cm]; ++i) {
            dest[i] = 255 - dest[i];
            src[i] = 255 - src[i];
        }
    }
    {
        for (i = 0; i < splashColorModeNComps[cm]; ++i) {
            blend[i] = dest[i] < src[i] ? dest[i] : src[i];
        }
    }
    if (cm == splashModeCMYK8 || cm == splashModeDeviceN8) {
        for (i = 0; i < splashColorModeNComps[cm]; ++i) {
            dest[i] = 255 - dest[i];
            src[i] = 255 - src[i];
            blend[i] = 255 - blend[i];
        }
    }
}

static void splashOutBlendLighten(SplashColorPtr src, SplashColorPtr dest, SplashColorPtr blend, SplashColorMode cm)
{
    int i;

    if (cm == splashModeCMYK8 || cm == splashModeDeviceN8) {
        for (i = 0; i < splashColorModeNComps[cm]; ++i) {
            dest[i] = 255 - dest[i];
            src[i] = 255 - src[i];
        }
    }
    {
        for (i = 0; i < splashColorModeNComps[cm]; ++i) {
            blend[i] = dest[i] > src[i] ? dest[i] : src[i];
        }
    }
    if (cm == splashModeCMYK8 || cm == splashModeDeviceN8) {
        for (i = 0; i < splashColorModeNComps[cm]; ++i) {
            dest[i] = 255 - dest[i];
            src[i] = 255 - src[i];
            blend[i] = 255 - blend[i];
        }
    }
}

static void splashOutBlendColorDodge(SplashColorPtr src, SplashColorPtr dest, SplashColorPtr blend, SplashColorMode cm)
{
    int i, x;

    if (cm == splashModeCMYK8 || cm == splashModeDeviceN8) {
        for (i = 0; i < splashColorModeNComps[cm]; ++i) {
            dest[i] = 255 - dest[i];
            src[i] = 255 - src[i];
        }
    }
    {
        for (i = 0; i < splashColorModeNComps[cm]; ++i) {
            if (src[i] == 255) {
                blend[i] = 255;
            } else {
                x = (dest[i] * 255) / (255 - src[i]);
                blend[i] = x <= 255 ? x : 255;
            }
        }
    }
    if (cm == splashModeCMYK8 || cm == splashModeDeviceN8) {
        for (i = 0; i < splashColorModeNComps[cm]; ++i) {
            dest[i] = 255 - dest[i];
            src[i] = 255 - src[i];
            blend[i] = 255 - blend[i];
        }
    }
}

static void splashOutBlendColorBurn(SplashColorPtr src, SplashColorPtr dest, SplashColorPtr blend, SplashColorMode cm)
{
    int i, x;

    if (cm == splashModeCMYK8 || cm == splashModeDeviceN8) {
        for (i = 0; i < splashColorModeNComps[cm]; ++i) {
            dest[i] = 255 - dest[i];
            src[i] = 255 - src[i];
        }
    }
    {
        for (i = 0; i < splashColorModeNComps[cm]; ++i) {
            if (src[i] == 0) {
                blend[i] = 0;
            } else {
                x = ((255 - dest[i]) * 255) / src[i];
                blend[i] = x <= 255 ? 255 - x : 0;
            }
        }
    }
    if (cm == splashModeCMYK8 || cm == splashModeDeviceN8) {
        for (i = 0; i < splashColorModeNComps[cm]; ++i) {
            dest[i] = 255 - dest[i];
            src[i] = 255 - src[i];
            blend[i] = 255 - blend[i];
        }
    }
}

static void splashOutBlendHardLight(SplashColorPtr src, SplashColorPtr dest, SplashColorPtr blend, SplashColorMode cm)
{
    int i;

    if (cm == splashModeCMYK8 || cm == splashModeDeviceN8) {
        for (i = 0; i < splashColorModeNComps[cm]; ++i) {
            dest[i] = 255 - dest[i];
            src[i] = 255 - src[i];
        }
    }
    {
        for (i = 0; i < splashColorModeNComps[cm]; ++i) {
            blend[i] = src[i] < 0x80 ? (dest[i] * 2 * src[i]) / 255 : 255 - 2 * ((255 - dest[i]) * (255 - src[i])) / 255;
        }
    }
    if (cm == splashModeCMYK8 || cm == splashModeDeviceN8) {
        for (i = 0; i < splashColorModeNComps[cm]; ++i) {
            dest[i] = 255 - dest[i];
            src[i] = 255 - src[i];
            blend[i] = 255 - blend[i];
        }
    }
}

static void splashOutBlendSoftLight(SplashColorPtr src, SplashColorPtr dest, SplashColorPtr blend, SplashColorMode cm)
{
    int i, x;

    if (cm == splashModeCMYK8 || cm == splashModeDeviceN8) {
        for (i = 0; i < splashColorModeNComps[cm]; ++i) {
            dest[i] = 255 - dest[i];
            src[i] = 255 - src[i];
        }
    }
    {
        for (i = 0; i < splashColorModeNComps[cm]; ++i) {
            if (src[i] < 0x80) {
                blend[i] = dest[i] - (255 - 2 * src[i]) * dest[i] * (255 - dest[i]) / (255 * 255);
            } else {
                if (dest[i] < 0x40) {
                    x = (((((16 * dest[i] - 12 * 255) * dest[i]) / 255) + 4 * 255) * dest[i]) / 255;
                } else {
                    x = static_cast<int>(sqrt(255.0 * dest[i]));
                }
                blend[i] = dest[i] + (2 * src[i] - 255) * (x - dest[i]) / 255;
            }
        }
    }
    if (cm == splashModeCMYK8 || cm == splashModeDeviceN8) {
        for (i = 0; i < splashColorModeNComps[cm]; ++i) {
            dest[i] = 255 - dest[i];
            src[i] = 255 - src[i];
            blend[i] = 255 - blend[i];
        }
    }
}

static void splashOutBlendDifference(SplashColorPtr src, SplashColorPtr dest, SplashColorPtr blend, SplashColorMode cm)
{
    int i;

    if (cm == splashModeCMYK8 || cm == splashModeDeviceN8) {
        for (i = 0; i < splashColorModeNComps[cm]; ++i) {
            dest[i] = 255 - dest[i];
            src[i] = 255 - src[i];
        }
    }
    {
        for (i = 0; i < splashColorModeNComps[cm]; ++i) {
            blend[i] = dest[i] < src[i] ? src[i] - dest[i] : dest[i] - src[i];
        }
    }
    if (cm == splashModeCMYK8 || cm == splashModeDeviceN8) {
        for (i = 0; i < splashColorModeNComps[cm]; ++i) {
            dest[i] = 255 - dest[i];
            src[i] = 255 - src[i];
            blend[i] = 255 - blend[i];
        }
    }
    if (cm == splashModeDeviceN8) {
        for (i = 4; i < splashColorModeNComps[cm]; ++i) {
            if (dest[i] == 0 && src[i] == 0) {
                blend[i] = 0;
            }
        }
    }
}

static void splashOutBlendExclusion(SplashColorPtr src, SplashColorPtr dest, SplashColorPtr blend, SplashColorMode cm)
{
    int i;

    if (cm == splashModeCMYK8 || cm == splashModeDeviceN8) {
        for (i = 0; i < splashColorModeNComps[cm]; ++i) {
            dest[i] = 255 - dest[i];
            src[i] = 255 - src[i];
        }
    }
    {
        for (i = 0; i < splashColorModeNComps[cm]; ++i) {
            blend[i] = dest[i] + src[i] - (2 * dest[i] * src[i]) / 255;
        }
    }
    if (cm == splashModeCMYK8 || cm == splashModeDeviceN8) {
        for (i = 0; i < splashColorModeNComps[cm]; ++i) {
            dest[i] = 255 - dest[i];
            src[i] = 255 - src[i];
            blend[i] = 255 - blend[i];
        }
    }
    if (cm == splashModeDeviceN8) {
        for (i = 4; i < splashColorModeNComps[cm]; ++i) {
            if (dest[i] == 0 && src[i] == 0) {
                blend[i] = 0;
            }
        }
    }
}

static int getLum(int r, int g, int b)
{
    // (int)(0.3 * r + 0.59 * g + 0.11 * b) =
    // (int)(256 / 256 * 0.3 * r + 256 / 256 * 0.59 * g + 256 / 256 * 0.11 * b)
    // (int)((77 * r + 151 * g + 28 * b) / 256)  = // round!
    return ((r * 77 + g * 151 + b * 28 + 0x80) >> 8);
}

static int getSat(int r, int g, int b)
{
    int rgbMin = std::min({ r, g, b });
    int rgbMax = std::max({ r, g, b });

    return rgbMax - rgbMin;
}

static void clipColor(int rIn, int gIn, int bIn, unsigned char *rOut, unsigned char *gOut, unsigned char *bOut)
{
    int lum = getLum(rIn, gIn, bIn);
    int rgbMin = std::min({ rIn, bIn, gIn });
    int rgbMax = std::max({ rIn, bIn, gIn });

    if (rgbMin < 0) {
        *rOut = static_cast<unsigned char>(std::clamp(lum + ((rIn - lum) * lum) / (lum - rgbMin), 0, 255));
        *gOut = static_cast<unsigned char>(std::clamp(lum + ((gIn - lum) * lum) / (lum - rgbMin), 0, 255));
        *bOut = static_cast<unsigned char>(std::clamp(lum + ((bIn - lum) * lum) / (lum - rgbMin), 0, 255));
    } else if (rgbMax > 255) {
        *rOut = static_cast<unsigned char>(std::clamp(lum + ((rIn - lum) * (255 - lum)) / (rgbMax - lum), 0, 255));
        *gOut = static_cast<unsigned char>(std::clamp(lum + ((gIn - lum) * (255 - lum)) / (rgbMax - lum), 0, 255));
        *bOut = static_cast<unsigned char>(std::clamp(lum + ((bIn - lum) * (255 - lum)) / (rgbMax - lum), 0, 255));
    } else {
        *rOut = rIn;
        *gOut = gIn;
        *bOut = bIn;
    }
}

static void setLum(unsigned char rIn, unsigned char gIn, unsigned char bIn, int lum, unsigned char *rOut, unsigned char *gOut, unsigned char *bOut)
{
    int d;

    d = lum - getLum(rIn, gIn, bIn);
    clipColor(rIn + d, gIn + d, bIn + d, rOut, gOut, bOut);
}

static void setSat(unsigned char rIn, unsigned char gIn, unsigned char bIn, int sat, unsigned char *rOut, unsigned char *gOut, unsigned char *bOut)
{
    int rgbMin, rgbMid, rgbMax;
    unsigned char *minOut, *midOut, *maxOut;

    if (rIn < gIn) {
        rgbMin = rIn;
        minOut = rOut;
        rgbMid = gIn;
        midOut = gOut;
    } else {
        rgbMin = gIn;
        minOut = gOut;
        rgbMid = rIn;
        midOut = rOut;
    }
    if (bIn > rgbMid) {
        rgbMax = bIn;
        maxOut = bOut;
    } else if (bIn > rgbMin) {
        rgbMax = rgbMid;
        maxOut = midOut;
        rgbMid = bIn;
        midOut = bOut;
    } else {
        rgbMax = rgbMid;
        maxOut = midOut;
        rgbMid = rgbMin;
        midOut = minOut;
        rgbMin = bIn;
        minOut = bOut;
    }
    if (rgbMax > rgbMin) {
        *midOut = static_cast<unsigned char>(std::clamp(((rgbMid - rgbMin) * sat) / (rgbMax - rgbMin), 0, 255));
        *maxOut = static_cast<unsigned char>(std::clamp(sat, 0, 255));
    } else {
        *midOut = *maxOut = 0;
    }
    *minOut = 0;
}

static void splashOutBlendHue(SplashColorPtr src, SplashColorPtr dest, SplashColorPtr blend, SplashColorMode cm)
{
    unsigned char r0, g0, b0;
    unsigned char r1, g1, b1;
    int i;
    SplashColor src2, dest2;

    switch (cm) {
    case splashModeMono1:
    case splashModeMono8:
        setSat(src[0], src[0], src[0], getSat(dest[0], dest[0], dest[0]), &r0, &g0, &b0);
        setLum(r0, g0, b0, getLum(dest[0], dest[0], dest[0]), &blend[0], &blend[0], &blend[0]);
        break;
    case splashModeXBGR8:
        src[3] = 255;
        // fallthrough
    case splashModeRGB8:
    case splashModeBGR8:
        setSat(src[0], src[1], src[2], getSat(dest[0], dest[1], dest[2]), &r0, &g0, &b0);
        setLum(r0, g0, b0, getLum(dest[0], dest[1], dest[2]), &blend[0], &blend[1], &blend[2]);
        break;
    case splashModeCMYK8:
    case splashModeDeviceN8:
        for (i = 0; i < 4; i++) {
            // convert to additive
            src2[i] = 0xff - src[i];
            dest2[i] = 0xff - dest[i];
        }
        // NB: inputs have already been converted to additive mode
        setSat(src2[0], src2[1], src2[2], getSat(dest2[0], dest2[1], dest2[2]), &r0, &g0, &b0);
        setLum(r0, g0, b0, getLum(dest2[0], dest2[1], dest2[2]), &r1, &g1, &b1);
        blend[0] = r1;
        blend[1] = g1;
        blend[2] = b1;
        blend[3] = dest2[3];
        for (i = 0; i < 4; i++) {
            // convert back to subtractive
            blend[i] = 0xff - blend[i];
        }
        break;
    }
}

static void splashOutBlendSaturation(SplashColorPtr src, SplashColorPtr dest, SplashColorPtr blend, SplashColorMode cm)
{
    unsigned char r0, g0, b0;
    unsigned char r1, g1, b1;
    int i;
    SplashColor src2, dest2;

    switch (cm) {
    case splashModeMono1:
    case splashModeMono8:
        setSat(dest[0], dest[0], dest[0], getSat(src[0], src[0], src[0]), &r0, &g0, &b0);
        setLum(r0, g0, b0, getLum(dest[0], dest[0], dest[0]), &blend[0], &blend[0], &blend[0]);
        break;
    case splashModeXBGR8:
        src[3] = 255;
        // fallthrough
    case splashModeRGB8:
    case splashModeBGR8:
        setSat(dest[0], dest[1], dest[2], getSat(src[0], src[1], src[2]), &r0, &g0, &b0);
        setLum(r0, g0, b0, getLum(dest[0], dest[1], dest[2]), &blend[0], &blend[1], &blend[2]);
        break;
    case splashModeCMYK8:
    case splashModeDeviceN8:
        for (i = 0; i < 4; i++) {
            // convert to additive
            src2[i] = 0xff - src[i];
            dest2[i] = 0xff - dest[i];
        }
        setSat(dest2[0], dest2[1], dest2[2], getSat(src2[0], src2[1], src2[2]), &r0, &g0, &b0);
        setLum(r0, g0, b0, getLum(dest2[0], dest2[1], dest2[2]), &r1, &g1, &b1);
        blend[0] = r1;
        blend[1] = g1;
        blend[2] = b1;
        blend[3] = dest2[3];
        for (i = 0; i < 4; i++) {
            // convert back to subtractive
            blend[i] = 0xff - blend[i];
        }
        break;
    }
}

static void splashOutBlendColor(SplashColorPtr src, SplashColorPtr dest, SplashColorPtr blend, SplashColorMode cm)
{
    unsigned char r, g, b;
    int i;
    SplashColor src2, dest2;

    switch (cm) {
    case splashModeMono1:
    case splashModeMono8:
        setLum(src[0], src[0], src[0], getLum(dest[0], dest[0], dest[0]), &blend[0], &blend[0], &blend[0]);
        break;
    case splashModeXBGR8:
        src[3] = 255;
        // fallthrough
    case splashModeRGB8:
    case splashModeBGR8:
        setLum(src[0], src[1], src[2], getLum(dest[0], dest[1], dest[2]), &blend[0], &blend[1], &blend[2]);
        break;
    case splashModeCMYK8:
    case splashModeDeviceN8:
        for (i = 0; i < 4; i++) {
            // convert to additive
            src2[i] = 0xff - src[i];
            dest2[i] = 0xff - dest[i];
        }
        setLum(src2[0], src2[1], src2[2], getLum(dest2[0], dest2[1], dest2[2]), &r, &g, &b);
        blend[0] = r;
        blend[1] = g;
        blend[2] = b;
        blend[3] = dest2[3];
        for (i = 0; i < 4; i++) {
            // convert back to subtractive
            blend[i] = 0xff - blend[i];
        }
        break;
    }
}

static void splashOutBlendLuminosity(SplashColorPtr src, SplashColorPtr dest, SplashColorPtr blend, SplashColorMode cm)
{
    unsigned char r, g, b;
    int i;
    SplashColor src2, dest2;

    switch (cm) {
    case splashModeMono1:
    case splashModeMono8:
        setLum(dest[0], dest[0], dest[0], getLum(src[0], src[0], src[0]), &blend[0], &blend[0], &blend[0]);
        break;
    case splashModeXBGR8:
        src[3] = 255;
        // fallthrough
    case splashModeRGB8:
    case splashModeBGR8:
        setLum(dest[0], dest[1], dest[2], getLum(src[0], src[1], src[2]), &blend[0], &blend[1], &blend[2]);
        break;
    case splashModeCMYK8:
    case splashModeDeviceN8:
        for (i = 0; i < 4; i++) {
            // convert to additive
            src2[i] = 0xff - src[i];
            dest2[i] = 0xff - dest[i];
        }
        setLum(dest2[0], dest2[1], dest2[2], getLum(src2[0], src2[1], src2[2]), &r, &g, &b);
        blend[0] = r;
        blend[1] = g;
        blend[2] = b;
        blend[3] = src2[3];
        for (i = 0; i < 4; i++) {
            // convert back to subtractive
            blend[i] = 0xff - blend[i];
        }
        break;
    }
}

// NB: This must match the GfxBlendMode enum defined in GfxState.h.
static const SplashBlendFunc splashOutBlendFuncs[] = { nullptr,
                                                       &splashOutBlendMultiply,
                                                       &splashOutBlendScreen,
                                                       &splashOutBlendOverlay,
                                                       &splashOutBlendDarken,
                                                       &splashOutBlendLighten,
                                                       &splashOutBlendColorDodge,
                                                       &splashOutBlendColorBurn,
                                                       &splashOutBlendHardLight,
                                                       &splashOutBlendSoftLight,
                                                       &splashOutBlendDifference,
                                                       &splashOutBlendExclusion,
                                                       &splashOutBlendHue,
                                                       &splashOutBlendSaturation,
                                                       &splashOutBlendColor,
                                                       &splashOutBlendLuminosity };

//------------------------------------------------------------------------
// SplashOutFontFileID
//------------------------------------------------------------------------

class SplashOutFontFileID : public SplashFontFileID
{
public:
    explicit SplashOutFontFileID(const Ref *rA) { r = *rA; }

    ~SplashOutFontFileID() override;

    bool matches(const SplashFontFileID &id) const override { return static_cast<const SplashOutFontFileID &>(id).r == r; }

private:
    Ref r;
};

SplashOutFontFileID::~SplashOutFontFileID() = default;

//------------------------------------------------------------------------
// T3FontCache
//------------------------------------------------------------------------

struct T3FontCacheTag
{
    unsigned short code;
    unsigned short mru; // valid bit (0x8000) and MRU index
};

class T3FontCache
{
public:
    T3FontCache(const Ref *fontID, double m11A, double m12A, double m21A, double m22A, int glyphXA, int glyphYA, int glyphWA, int glyphHA, bool validBBoxA, bool aa);
    ~T3FontCache();
    T3FontCache(const T3FontCache &) = delete;
    T3FontCache &operator=(const T3FontCache &) = delete;
    bool matches(const Ref *idA, double m11A, double m12A, double m21A, double m22A) const { return fontID == *idA && m11 == m11A && m12 == m12A && m21 == m21A && m22 == m22A; }

    Ref fontID; // PDF font ID
    double m11, m12, m21, m22; // transform matrix
    int glyphX, glyphY; // pixel offset of glyph bitmaps
    int glyphW, glyphH; // size of glyph bitmaps, in pixels
    bool validBBox; // false if the bbox was [0 0 0 0]
    int glyphSize; // size of glyph bitmaps, in bytes
    int cacheSets; // number of sets in cache
    int cacheAssoc; // cache associativity (glyphs per set)
    unsigned char *cacheData; // glyph pixmap cache
    T3FontCacheTag *cacheTags; // cache tags, i.e., char codes
};

T3FontCache::T3FontCache(const Ref *fontIDA, double m11A, double m12A, double m21A, double m22A, int glyphXA, int glyphYA, int glyphWA, int glyphHA, bool validBBoxA, bool aa)
{

    fontID = *fontIDA;
    m11 = m11A;
    m12 = m12A;
    m21 = m21A;
    m22 = m22A;
    glyphX = glyphXA;
    glyphY = glyphYA;
    glyphW = glyphWA;
    glyphH = glyphHA;
    validBBox = validBBoxA;
    // sanity check for excessively large glyphs (which most likely
    // indicate an incorrect BBox)
    if (glyphW > INT_MAX / glyphH || glyphW <= 0 || glyphH <= 0 || glyphW * glyphH > 100000) {
        glyphW = glyphH = 100;
        validBBox = false;
    }
    if (aa) {
        glyphSize = glyphW * glyphH;
    } else {
        glyphSize = ((glyphW + 7) >> 3) * glyphH;
    }
    cacheAssoc = type3FontCacheAssoc;
    for (cacheSets = type3FontCacheMaxSets; cacheSets > 1 && cacheSets * cacheAssoc * glyphSize > type3FontCacheSize; cacheSets >>= 1) {
        ;
    }
    if (glyphSize < 10485760 / cacheAssoc / cacheSets) {
        cacheData = static_cast<unsigned char *>(gmallocn_checkoverflow(cacheSets * cacheAssoc, glyphSize));
    } else {
        error(errSyntaxWarning, -1,
              "Not creating cacheData for T3FontCache, it asked for too much memory.\n"
              "       This could teoretically result in wrong rendering,\n"
              "       but most probably the document is bogus.\n"
              "       Please report a bug if you think the rendering may be wrong because of this.");
        cacheData = nullptr;
    }
    if (cacheData != nullptr) {
        cacheTags = static_cast<T3FontCacheTag *>(gmallocn(cacheSets * cacheAssoc, sizeof(T3FontCacheTag)));
        for (int i = 0; i < cacheSets * cacheAssoc; ++i) {
            cacheTags[i].mru = i & (cacheAssoc - 1);
        }
    } else {
        cacheTags = nullptr;
    }
}

T3FontCache::~T3FontCache()
{
    gfree(cacheData);
    gfree(cacheTags);
}

struct T3GlyphStack
{
    unsigned short code; // character code

    bool haveDx; // set after seeing a d0/d1 operator
    bool doNotCache; // set if we see a gsave/grestore before
                     //   the d0/d1

    //----- cache info
    T3FontCache *cache; // font cache for the current font
    T3FontCacheTag *cacheTag; // pointer to cache tag for the glyph
    unsigned char *cacheData; // pointer to cache data for the glyph

    //----- saved state
    SplashBitmap *origBitmap;
    Splash *origSplash;
    double origCTM4, origCTM5;

    T3GlyphStack *next; // next object on stack
};

//------------------------------------------------------------------------
// SplashTransparencyGroup
//------------------------------------------------------------------------

struct SplashTransparencyGroup
{
    int tx, ty; // translation coordinates
    SplashBitmap *tBitmap; // bitmap for transparency group
    SplashBitmap *softmask; // bitmap for softmasks
    GfxColorSpace *blendingColorSpace;
    bool isolated;

    //----- for knockout
    SplashBitmap *shape;
    bool knockout;
    double knockoutOpacity;
    bool fontAA;

    //----- saved state
    SplashBitmap *origBitmap;
    Splash *origSplash;
    SplashBitmap *backdropBitmap;

    SplashTransparencyGroup *next;
};

//------------------------------------------------------------------------
// SplashOutputDev
//------------------------------------------------------------------------

SplashOutputDev::SplashOutputDev(SplashColorMode colorModeA, int bitmapRowPadA, SplashColorPtr paperColorA, bool bitmapTopDownA, SplashThinLineMode thinLineMode, bool overprintPreviewA)
{
    colorMode = colorModeA;
    bitmapRowPad = bitmapRowPadA;
    bitmapTopDown = bitmapTopDownA;
    fontAntialias = true;
    vectorAntialias = true;
    overprintPreview = overprintPreviewA;
    enableFreeType = true;
    enableFreeTypeHinting = false;
    enableSlightHinting = false;
    setupScreenParams(72.0, 72.0);
    if (paperColorA != nullptr) {
        splashColorCopy(paperColor, paperColorA);
    } else {
        splashClearColor(paperColor);
    }
    skipHorizText = false;
    skipRotatedText = false;
    keepAlphaChannel = paperColorA == nullptr;

    doc = nullptr;

    bitmap = new SplashBitmap(1, 1, bitmapRowPad, colorMode, colorMode != splashModeMono1, bitmapTopDown);
    splash = new Splash(bitmap, vectorAntialias, &screenParams);
    splash->setMinLineWidth(s_minLineWidth);
    splash->setThinLineMode(thinLineMode);
    splash->clear(paperColor, 0);

    fontEngine = nullptr;

    nT3Fonts = 0;
    t3GlyphStack = nullptr;

    font = nullptr;
    needFontUpdate = false;
    textClipPath = nullptr;
    transpGroupStack = nullptr;
    xref = nullptr;
}

void SplashOutputDev::setupScreenParams(double hDPI, double vDPI)
{
    screenParams.size = -1;
    screenParams.dotRadius = -1;

    // use clustered dithering for resolution >= 300 dpi
    // (compare to 299.9 to avoid floating point issues)
    if (hDPI > 299.9 && vDPI > 299.9) {
        screenParams.type = splashScreenStochasticClustered;
        if (screenParams.size < 0) {
            screenParams.size = 64;
        }
        if (screenParams.dotRadius < 0) {
            screenParams.dotRadius = 2;
        }
    } else {
        screenParams.type = splashScreenDispersed;
        if (screenParams.size < 0) {
            screenParams.size = 4;
        }
    }
}

SplashOutputDev::~SplashOutputDev()
{
    int i;

    for (i = 0; i < nT3Fonts; ++i) {
        delete t3FontCache[i];
    }
    delete fontEngine;
    delete splash;
    delete bitmap;
    delete textClipPath;
}

void SplashOutputDev::startDoc(PDFDoc *docA)
{
    int i;

    doc = docA;
    delete fontEngine;
    fontEngine = new SplashFontEngine(enableFreeType, enableFreeTypeHinting, enableSlightHinting, getFontAntialias() && colorMode != splashModeMono1);
    for (i = 0; i < nT3Fonts; ++i) {
        delete t3FontCache[i];
    }
    nT3Fonts = 0;
}

void SplashOutputDev::startPage(int /*pageNum*/, GfxState *state, XRef *xrefA)
{
    int w, h;
    SplashColor color;

    xref = xrefA;
    if (state) {
        setupScreenParams(state->getHDPI(), state->getVDPI());
        w = static_cast<int>(state->getPageWidth() + 0.5);
        if (w <= 0) {
            w = 1;
        }
        h = static_cast<int>(state->getPageHeight() + 0.5);
        if (h <= 0) {
            h = 1;
        }
    } else {
        w = h = 1;
    }
    SplashThinLineMode thinLineMode = splashThinLineDefault;
    if (splash) {
        thinLineMode = splash->getThinLineMode();
        delete splash;
        splash = nullptr;
    }
    if (!bitmap || w != bitmap->getWidth() || h != bitmap->getHeight()) {
        if (bitmap) {
            delete bitmap;
            bitmap = nullptr;
        }
        bitmap = new SplashBitmap(w, h, bitmapRowPad, colorMode, colorMode != splashModeMono1, bitmapTopDown);
        if (!bitmap->getDataPtr()) {
            delete bitmap;
            w = h = 1;
            bitmap = new SplashBitmap(w, h, bitmapRowPad, colorMode, colorMode != splashModeMono1, bitmapTopDown);
        }
    }
    splash = new Splash(bitmap, vectorAntialias, &screenParams);
    splash->setThinLineMode(thinLineMode);
    splash->setMinLineWidth(s_minLineWidth);
    if (state) {
        splash->setMatrix(state->getCTM());
    }
    switch (colorMode) {
    case splashModeMono1:
    case splashModeMono8:
        color[0] = 0;
        break;
    case splashModeXBGR8:
        color[3] = 255;
        // fallthrough
    case splashModeRGB8:
    case splashModeBGR8:
        color[0] = color[1] = color[2] = 0;
        break;
    case splashModeCMYK8:
        color[0] = color[1] = color[2] = color[3] = 0;
        break;
    case splashModeDeviceN8:
        splashClearColor(color);
        break;
    }
    splash->setStrokePattern(new SplashSolidColor(color));
    splash->setFillPattern(new SplashSolidColor(color));
    splash->setLineCap(SplashLineCap::Butt);
    splash->setLineJoin(SplashLineJoin::Miter);
    splash->setLineDash({}, 0);
    splash->setMiterLimit(10);
    splash->setFlatness(1);
    // the SA parameter supposedly defaults to false, but Acrobat
    // apparently hardwires it to true
    splash->setStrokeAdjust(true);
    splash->clear(paperColor, 0);
}

void SplashOutputDev::endPage()
{
    if (colorMode != splashModeMono1 && !keepAlphaChannel) {
        splash->compositeBackground(paperColor);
    }
}

void SplashOutputDev::saveState(GfxState * /*state*/)
{
    splash->saveState();
    if (t3GlyphStack && !t3GlyphStack->haveDx) {
        t3GlyphStack->doNotCache = true;
        error(errSyntaxWarning, -1, "Save (q) operator before d0/d1 in Type 3 glyph");
    }
}

void SplashOutputDev::restoreState(GfxState * /*state*/)
{
    splash->restoreState();
    needFontUpdate = true;
    if (t3GlyphStack && !t3GlyphStack->haveDx) {
        t3GlyphStack->doNotCache = true;
        error(errSyntaxWarning, -1, "Restore (Q) operator before d0/d1 in Type 3 glyph");
    }
}

void SplashOutputDev::updateAll(GfxState *state)
{
    updateLineDash(state);
    updateLineJoin(state);
    updateLineCap(state);
    updateLineWidth(state);
    updateFlatness(state);
    updateMiterLimit(state);
    updateStrokeAdjust(state);
    updateFillColorSpace(state);
    updateFillColor(state);
    updateStrokeColorSpace(state);
    updateStrokeColor(state);
    needFontUpdate = true;
}

void SplashOutputDev::updateCTM(GfxState *state, double /*m11*/, double /*m12*/, double /*m21*/, double /*m22*/, double /*m31*/, double /*m32*/)
{
    splash->setMatrix(state->getCTM());
}

void SplashOutputDev::updateLineDash(GfxState *state)
{
    double dashStart;

    std::vector<double> dash { state->getLineDash(&dashStart) };

    for (auto &elem : dash) {
        if (elem < 0) {
            elem = 0;
        }
    }

    splash->setLineDash(std::move(dash), dashStart);
}

void SplashOutputDev::updateFlatness(GfxState * /*state*/)
{
#if 0 // Acrobat ignores the flatness setting, and always renders curves
      // with a fairly small flatness value
   splash->setFlatness(state->getFlatness());
#endif
}

void SplashOutputDev::updateLineJoin(GfxState *state)
{
    static_assert(static_cast<SplashLineJoin>(GfxState::LineJoinMitre) == SplashLineJoin::Miter);
    static_assert(static_cast<SplashLineJoin>(GfxState::LineJoinRound) == SplashLineJoin::Round);
    static_assert(static_cast<SplashLineJoin>(GfxState::LineJoinBevel) == SplashLineJoin::Bevel);
    splash->setLineJoin(static_cast<SplashLineJoin>(state->getLineJoin()));
}

void SplashOutputDev::updateLineCap(GfxState *state)
{
    static_assert(static_cast<SplashLineCap>(GfxState::LineCapButt) == SplashLineCap::Butt);
    static_assert(static_cast<SplashLineCap>(GfxState::LineCapRound) == SplashLineCap::Round);
    static_assert(static_cast<SplashLineCap>(GfxState::LineCapProjecting) == SplashLineCap::Projecting);
    splash->setLineCap(static_cast<SplashLineCap>(state->getLineCap()));
}

void SplashOutputDev::updateMiterLimit(GfxState *state)
{
    splash->setMiterLimit(state->getMiterLimit());
}

void SplashOutputDev::updateLineWidth(GfxState *state)
{
    splash->setLineWidth(state->getLineWidth());
}

void SplashOutputDev::updateStrokeAdjust(GfxState * /*state*/)
{
#if 0 // the SA parameter supposedly defaults to false, but Acrobat
      // apparently hardwires it to true
  splash->setStrokeAdjust(state->getStrokeAdjust());
#endif
}

void SplashOutputDev::updateFillColorSpace(GfxState *state)
{
    if (colorMode == splashModeDeviceN8) {
        state->getFillColorSpace()->createMapping(bitmap->getSeparationList(), SPOT_NCOMPS);
    }
}

void SplashOutputDev::updateStrokeColorSpace(GfxState *state)
{
    if (colorMode == splashModeDeviceN8) {
        state->getStrokeColorSpace()->createMapping(bitmap->getSeparationList(), SPOT_NCOMPS);
    }
}

void SplashOutputDev::updateFillColor(GfxState *state)
{
    GfxGray gray;
    GfxRGB rgb;
    GfxCMYK cmyk;
    GfxColor deviceN;

    switch (colorMode) {
    case splashModeMono1:
    case splashModeMono8:
        state->getFillGray(&gray);
        splash->setFillPattern(getColor(gray));
        break;
    case splashModeXBGR8:
    case splashModeRGB8:
    case splashModeBGR8:
        state->getFillRGB(&rgb);
        splash->setFillPattern(getColor(&rgb));
        break;
    case splashModeCMYK8:
        state->getFillCMYK(&cmyk);
        splash->setFillPattern(getColor(&cmyk));
        break;
    case splashModeDeviceN8:
        state->getFillDeviceN(&deviceN);
        splash->setFillPattern(getColor(&deviceN));
        break;
    }
}

void SplashOutputDev::updateStrokeColor(GfxState *state)
{
    GfxGray gray;
    GfxRGB rgb;
    GfxCMYK cmyk;
    GfxColor deviceN;

    switch (colorMode) {
    case splashModeMono1:
    case splashModeMono8:
        state->getStrokeGray(&gray);
        splash->setStrokePattern(getColor(gray));
        break;
    case splashModeXBGR8:
    case splashModeRGB8:
    case splashModeBGR8:
        state->getStrokeRGB(&rgb);
        splash->setStrokePattern(getColor(&rgb));
        break;
    case splashModeCMYK8:
        state->getStrokeCMYK(&cmyk);
        splash->setStrokePattern(getColor(&cmyk));
        break;
    case splashModeDeviceN8:
        state->getStrokeDeviceN(&deviceN);
        splash->setStrokePattern(getColor(&deviceN));
        break;
    }
}

SplashPattern *SplashOutputDev::getColor(GfxGray gray)
{
    SplashColor color;

    color[0] = colToByte(gray);
    return new SplashSolidColor(color);
}

SplashPattern *SplashOutputDev::getColor(GfxRGB *rgb)
{
    GfxColorComp r, g, b;
    SplashColor color;

    r = rgb->r;
    g = rgb->g;
    b = rgb->b;
    color[0] = colToByte(r);
    color[1] = colToByte(g);
    color[2] = colToByte(b);
    if (colorMode == splashModeXBGR8) {
        color[3] = 255;
    }
    return new SplashSolidColor(color);
}

SplashPattern *SplashOutputDev::getColor(GfxCMYK *cmyk)
{
    SplashColor color;

    color[0] = colToByte(cmyk->c);
    color[1] = colToByte(cmyk->m);
    color[2] = colToByte(cmyk->y);
    color[3] = colToByte(cmyk->k);
    return new SplashSolidColor(color);
}

SplashPattern *SplashOutputDev::getColor(GfxColor *deviceN)
{
    SplashColor color;

    for (int i = 0; i < 4 + SPOT_NCOMPS; i++) {
        color[i] = colToByte(deviceN->c[i]);
    }
    return new SplashSolidColor(color);
}

void SplashOutputDev::getMatteColor(SplashColorMode colorMode, GfxImageColorMap *colorMap, const GfxColor &matteColorIn, SplashColor matteColor)
{
    GfxGray gray;
    GfxRGB rgb;
    GfxCMYK cmyk;
    GfxColor deviceN;

    switch (colorMode) {
    case splashModeMono1:
    case splashModeMono8:
        colorMap->getColorSpace()->getGray(matteColorIn, &gray);
        matteColor[0] = colToByte(gray);
        break;
    case splashModeRGB8:
    case splashModeBGR8:
        colorMap->getColorSpace()->getRGB(matteColorIn, &rgb);
        matteColor[0] = colToByte(rgb.r);
        matteColor[1] = colToByte(rgb.g);
        matteColor[2] = colToByte(rgb.b);
        break;
    case splashModeXBGR8:
        colorMap->getColorSpace()->getRGB(matteColorIn, &rgb);
        matteColor[0] = colToByte(rgb.r);
        matteColor[1] = colToByte(rgb.g);
        matteColor[2] = colToByte(rgb.b);
        matteColor[3] = 255;
        break;
    case splashModeCMYK8:
        colorMap->getColorSpace()->getCMYK(matteColorIn, &cmyk);
        matteColor[0] = colToByte(cmyk.c);
        matteColor[1] = colToByte(cmyk.m);
        matteColor[2] = colToByte(cmyk.y);
        matteColor[3] = colToByte(cmyk.k);
        break;
    case splashModeDeviceN8:
        colorMap->getColorSpace()->getDeviceN(matteColorIn, &deviceN);
        for (int cp = 0; cp < SPOT_NCOMPS + 4; cp++) {
            matteColor[cp] = colToByte(deviceN.c[cp]);
        }
        break;
    }
}

void SplashOutputDev::setOverprintMask(GfxColorSpace *colorSpace, bool overprintFlag, int overprintMode, const GfxColor *singleColor, bool grayIndexed)
{
    unsigned int mask;
    GfxCMYK cmyk;
    bool additive = false;
    int i;

    if (colorSpace->getMode() == csIndexed) {
        setOverprintMask((static_cast<GfxIndexedColorSpace *>(colorSpace))->getBase(), overprintFlag, overprintMode, singleColor, grayIndexed);
        return;
    }
    if (overprintFlag && overprintPreview) {
        mask = colorSpace->getOverprintMask();
        if (singleColor && overprintMode && colorSpace->getMode() == csDeviceCMYK) {
            colorSpace->getCMYK(*singleColor, &cmyk);
            if (cmyk.c == 0) {
                mask &= ~1;
            }
            if (cmyk.m == 0) {
                mask &= ~2;
            }
            if (cmyk.y == 0) {
                mask &= ~4;
            }
            if (cmyk.k == 0) {
                mask &= ~8;
            }
        }
        if (grayIndexed && colorSpace->getMode() != csDeviceN) {
            mask &= ~7;
        } else if (colorSpace->getMode() == csSeparation) {
            auto *deviceSep = static_cast<GfxSeparationColorSpace *>(colorSpace);
            additive = deviceSep->getName()->compare("All") != 0 && mask == 0x0f && !deviceSep->isNonMarking();
        } else if (colorSpace->getMode() == csDeviceN) {
            auto *deviceNCS = static_cast<GfxDeviceNColorSpace *>(colorSpace);
            additive = mask == 0x0f && !deviceNCS->isNonMarking();
            for (i = 0; i < deviceNCS->getNComps() && additive; i++) {
                if (deviceNCS->getColorantName(i) == "Cyan") {
                    additive = false;
                } else if (deviceNCS->getColorantName(i) == "Magenta") {
                    additive = false;
                } else if (deviceNCS->getColorantName(i) == "Yellow") {
                    additive = false;
                } else if (deviceNCS->getColorantName(i) == "Black") {
                    additive = false;
                }
            }
        }
    } else {
        mask = 0xffffffff;
    }
    splash->setOverprintMask(mask, additive);
}

void SplashOutputDev::updateBlendMode(GfxState *state)
{
    splash->setBlendFunc(splashOutBlendFuncs[state->getBlendMode()]);
}

void SplashOutputDev::updateFillOpacity(GfxState *state)
{
    splash->setFillAlpha(state->getFillOpacity());
    if (transpGroupStack != nullptr && state->getFillOpacity() < transpGroupStack->knockoutOpacity) {
        transpGroupStack->knockoutOpacity = state->getFillOpacity();
    }
}

void SplashOutputDev::updateStrokeOpacity(GfxState *state)
{
    splash->setStrokeAlpha(state->getStrokeOpacity());
    if (transpGroupStack != nullptr && state->getStrokeOpacity() < transpGroupStack->knockoutOpacity) {
        transpGroupStack->knockoutOpacity = state->getStrokeOpacity();
    }
}

void SplashOutputDev::updatePatternOpacity(GfxState *state)
{
    splash->setPatternAlpha(state->getStrokeOpacity(), state->getFillOpacity());
}

void SplashOutputDev::clearPatternOpacity(GfxState * /*state*/)
{
    splash->clearPatternAlpha();
}

void SplashOutputDev::updateFillOverprint(GfxState *state)
{
    splash->setFillOverprint(state->getFillOverprint());
}

void SplashOutputDev::updateStrokeOverprint(GfxState *state)
{
    splash->setStrokeOverprint(state->getStrokeOverprint());
}

void SplashOutputDev::updateOverprintMode(GfxState *state)
{
    splash->setOverprintMode(state->getOverprintMode());
}

void SplashOutputDev::updateTransfer(GfxState *state)
{
    unsigned char red[256], green[256], blue[256], gray[256];
    double x, y;
    int i;

    const std::vector<std::unique_ptr<Function>> &transfer = state->getTransfer();
    if (!transfer.empty() && transfer[0]->getInputSize() == 1 && transfer[0]->getOutputSize() == 1) {
        if (transfer.size() == 4 && transfer[1]->getInputSize() == 1 && transfer[1]->getOutputSize() == 1 && transfer[2]->getInputSize() == 1 && transfer[2]->getOutputSize() == 1 && transfer[3]->getInputSize() == 1
            && transfer[3]->getOutputSize() == 1) {
            for (i = 0; i < 256; ++i) {
                x = i / 255.0;
                transfer[0]->transform(&x, &y);
                red[i] = static_cast<unsigned char>(y * 255.0 + 0.5);
                transfer[1]->transform(&x, &y);
                green[i] = static_cast<unsigned char>(y * 255.0 + 0.5);
                transfer[2]->transform(&x, &y);
                blue[i] = static_cast<unsigned char>(y * 255.0 + 0.5);
                transfer[3]->transform(&x, &y);
                gray[i] = static_cast<unsigned char>(y * 255.0 + 0.5);
            }
        } else {
            for (i = 0; i < 256; ++i) {
                x = i / 255.0;
                transfer[0]->transform(&x, &y);
                red[i] = green[i] = blue[i] = gray[i] = static_cast<unsigned char>(y * 255.0 + 0.5);
            }
        }
    } else {
        for (i = 0; i < 256; ++i) {
            red[i] = green[i] = blue[i] = gray[i] = static_cast<unsigned char>(i);
        }
    }
    splash->setTransfer(red, green, blue, gray);
}

void SplashOutputDev::updateFont(GfxState * /*state*/)
{
    needFontUpdate = true;
}

void SplashOutputDev::doUpdateFont(GfxState *state)
{
    GfxFontType fontType;
    std::shared_ptr<SplashFontFile> fontFile;
    std::unique_ptr<SplashOutFontFileID> id;
    double m11, m12, m21, m22, fontSize;
    std::array<double, 4> mat;
    bool recreateFont = false;
    bool doAdjustFontMatrix = false;

    needFontUpdate = false;
    font = nullptr;

    GfxFont *const gfxFont = state->getFont().get();
    if (!gfxFont) {
        return;
    }
    fontType = gfxFont->getType();
    if (fontType == fontType3) {
        return;
    }

    // sanity-check the font size - skip anything larger than 10 inches
    // (this avoids problems allocating memory for the font cache)
    if (state->getTransformedFontSize() > 10 * (state->getHDPI() + state->getVDPI())) {
        return;
    }

    // check the font file cache
reload:
    id = std::make_unique<SplashOutFontFileID>(gfxFont->getID());
    if ((fontFile = fontEngine->getFontFile(*id))) {
        id.reset();
    } else {

        std::optional<GfxFontLoc> fontLoc = gfxFont->locateFont((xref) ? xref : doc->getXRef(), nullptr);
        if (!fontLoc) {
            error(errSyntaxError, -1, "Couldn't find a font for '{0:s}'", gfxFont->getName() ? gfxFont->getName()->c_str() : "(unnamed)");
            return;
        }

        // embedded font
        std::string fileName;
        std::optional<std::vector<unsigned char>> tmpBuf;

        if (fontLoc->locType == gfxFontLocEmbedded) {
            // if there is an embedded font, read it to memory
            tmpBuf = gfxFont->readEmbFontFile((xref) ? xref : doc->getXRef());
            if (!tmpBuf) {
                return;
            }

            // external font
        } else { // gfxFontLocExternal
            fileName = fontLoc->path;
            fontType = fontLoc->fontType;
            doAdjustFontMatrix = true;
        }

        std::unique_ptr<SplashFontSrc> fontsrc;
        if (!fileName.empty()) {
            fontsrc = std::make_unique<SplashFontSrc>(fileName);
        } else {
            fontsrc = std::make_unique<SplashFontSrc>(std::move(tmpBuf.value()));
        }

        // load the font file
        switch (fontType) {
        case fontType1:
            if (!(fontFile = fontEngine->loadType1Font(std::move(id), std::move(fontsrc), static_cast<Gfx8BitFont *>(gfxFont)->getEncoding(), fontLoc->fontNum))) {
                error(errSyntaxError, -1, "Couldn't create a font for '{0:s}'", gfxFont->getName() ? gfxFont->getName()->c_str() : "(unnamed)");
                if (gfxFont->invalidateEmbeddedFont()) {
                    goto reload;
                }
                return;
            }
            break;
        case fontType1C:
            if (!(fontFile = fontEngine->loadType1CFont(std::move(id), std::move(fontsrc), static_cast<Gfx8BitFont *>(gfxFont)->getEncoding(), fontLoc->fontNum))) {
                error(errSyntaxError, -1, "Couldn't create a font for '{0:s}'", gfxFont->getName() ? gfxFont->getName()->c_str() : "(unnamed)");
                if (gfxFont->invalidateEmbeddedFont()) {
                    goto reload;
                }
                return;
            }
            break;
        case fontType1COT:
            if (!(fontFile = fontEngine->loadOpenTypeT1CFont(std::move(id), std::move(fontsrc), static_cast<Gfx8BitFont *>(gfxFont)->getEncoding(), fontLoc->fontNum))) {
                error(errSyntaxError, -1, "Couldn't create a font for '{0:s}'", gfxFont->getName() ? gfxFont->getName()->c_str() : "(unnamed)");
                if (gfxFont->invalidateEmbeddedFont()) {
                    goto reload;
                }
                return;
            }
            break;
        case fontTrueType:
        case fontTrueTypeOT: {
            std::unique_ptr<FoFiTrueType> ff;
            if (!fileName.empty()) {
                ff = FoFiTrueType::load(fileName.c_str(), fontLoc->fontNum);
            } else {
                ff = FoFiTrueType::make(fontsrc->buf(), fontLoc->fontNum);
            }
            std::vector<int> codeToGID;
            if (ff) {
                codeToGID = (static_cast<Gfx8BitFont *>(gfxFont))->getCodeToGIDMap(ff.get());
                // if we're substituting for a non-TrueType font, we need to mark
                // all notdef codes as "do not draw" (rather than drawing TrueType
                // notdef glyphs)
                if (gfxFont->getType() != fontTrueType && gfxFont->getType() != fontTrueTypeOT) {
                    for (int i = 0; i < 256; ++i) {
                        if (codeToGID[i] == 0) {
                            codeToGID[i] = -1;
                        }
                    }
                }
            }
            if (!(fontFile = fontEngine->loadTrueTypeFont(std::move(id), std::move(fontsrc), std::move(codeToGID), fontLoc->fontNum))) {
                error(errSyntaxError, -1, "Couldn't create a font for '{0:s}'", gfxFont->getName() ? gfxFont->getName()->c_str() : "(unnamed)");
                if (gfxFont->invalidateEmbeddedFont()) {
                    goto reload;
                }
                return;
            }
            break;
        }
        case fontCIDType0:
        case fontCIDType0C:
            if (!(fontFile = fontEngine->loadCIDFont(std::move(id), std::move(fontsrc), fontLoc->fontNum))) {
                error(errSyntaxError, -1, "Couldn't create a font for '{0:s}'", gfxFont->getName() ? gfxFont->getName()->c_str() : "(unnamed)");
                if (gfxFont->invalidateEmbeddedFont()) {
                    goto reload;
                }
                return;
            }
            break;
        case fontCIDType0COT: {
            std::vector<int> codeToGID;
            if ((static_cast<GfxCIDFont *>(gfxFont))->getCIDToGIDLen() > 0) {
                codeToGID = (static_cast<GfxCIDFont *>(gfxFont))->getCIDToGID();
            } else {
            }
            if (!(fontFile = fontEngine->loadOpenTypeCFFFont(std::move(id), std::move(fontsrc), std::move(codeToGID), fontLoc->fontNum))) {
                error(errSyntaxError, -1, "Couldn't create a font for '{0:s}'", gfxFont->getName() ? gfxFont->getName()->c_str() : "(unnamed)");
                if (gfxFont->invalidateEmbeddedFont()) {
                    goto reload;
                }
                return;
            }
            break;
        }
        case fontCIDType2:
        case fontCIDType2OT: {
            std::vector<int> codeToGID;
            if ((static_cast<GfxCIDFont *>(gfxFont))->getCIDToGIDLen() > 0) {
                codeToGID = (static_cast<GfxCIDFont *>(gfxFont))->getCIDToGID();
            } else {
                std::unique_ptr<FoFiTrueType> ff;
                if (!fileName.empty()) {
                    ff = FoFiTrueType::load(fileName.c_str(), fontLoc->fontNum);
                } else {
                    ff = FoFiTrueType::make(fontsrc->buf(), fontLoc->fontNum);
                }
                if (!ff) {
                    error(errSyntaxError, -1, "Couldn't create a font for '{0:s}'", gfxFont->getName() ? gfxFont->getName()->c_str() : "(unnamed)");
                    return;
                }
                codeToGID = (static_cast<GfxCIDFont *>(gfxFont))->getCodeToGIDMap(ff.get());
            }
            if (!(fontFile = fontEngine->loadTrueTypeFont(std::move(id), std::move(fontsrc), std::move(codeToGID), fontLoc->fontNum))) {
                error(errSyntaxError, -1, "Couldn't create a font for '{0:s}'", gfxFont->getName() ? gfxFont->getName()->c_str() : "(unnamed)");
                if (gfxFont->invalidateEmbeddedFont()) {
                    goto reload;
                }
                return;
            }
            break;
        }
        default:
            // this shouldn't happen
            return;
        }
        fontFile->doAdjustMatrix = doAdjustFontMatrix;
    }

    // get the font matrix
    const std::array<double, 6> &textMat = state->getTextMat();
    fontSize = state->getFontSize();
    m11 = textMat[0] * fontSize * state->getHorizScaling();
    m12 = textMat[1] * fontSize * state->getHorizScaling();
    m21 = textMat[2] * fontSize;
    m22 = textMat[3] * fontSize;

    // create the scaled font
    mat[0] = m11;
    mat[1] = m12;
    mat[2] = m21;
    mat[3] = m22;
    font = fontEngine->getFont(fontFile, mat, splash->getMatrix());

    // for substituted fonts: adjust the font matrix -- compare the
    // width of 'm' in the original font and the substituted font
    if (fontFile->doAdjustMatrix && !gfxFont->isCIDFont()) {
        double w1, w2, w3;
        CharCode code;
        const char *name;
        for (code = 0; code < 256; ++code) {
            if ((name = (static_cast<Gfx8BitFont *>(gfxFont))->getCharName(code)) && name[0] == 'm' && name[1] == '\0') {
                break;
            }
        }
        if (code < 256) {
            w1 = (static_cast<Gfx8BitFont *>(gfxFont))->getWidth(code);
            w2 = font->getGlyphAdvance(code);
            w3 = (static_cast<Gfx8BitFont *>(gfxFont))->getWidth(0);
            if (!gfxFont->isSymbolic() && w2 > 0 && w1 > w3) {
                // if real font is substantially narrower than substituted
                // font, reduce the font size accordingly
                if (w1 > 0.01 && w1 < 0.9 * w2) {
                    w1 /= w2;
                    m11 *= w1;
                    m21 *= w1;
                    recreateFont = true;
                }
            }
        }
    }

    if (recreateFont) {
        mat[0] = m11;
        mat[1] = m12;
        mat[2] = m21;
        mat[3] = m22;
        font = fontEngine->getFont(fontFile, mat, splash->getMatrix());
    }
}

void SplashOutputDev::stroke(GfxState *state)
{
    if (state->getStrokeColorSpace()->isNonMarking()) {
        return;
    }
    setOverprintMask(state->getStrokeColorSpace(), state->getStrokeOverprint(), state->getOverprintMode(), &state->getStrokeColor());
    const SplashPath path = convertPath(state->getPath(), false);
    splash->stroke(path);
}

void SplashOutputDev::fill(GfxState *state)
{
    if (state->getFillColorSpace()->isNonMarking()) {
        return;
    }
    setOverprintMask(state->getFillColorSpace(), state->getFillOverprint(), state->getOverprintMode(), &state->getFillColor());
    SplashPath path = convertPath(state->getPath(), true);
    splash->fill(&path, false);
}

void SplashOutputDev::eoFill(GfxState *state)
{
    if (state->getFillColorSpace()->isNonMarking()) {
        return;
    }
    setOverprintMask(state->getFillColorSpace(), state->getFillOverprint(), state->getOverprintMode(), &state->getFillColor());
    SplashPath path = convertPath(state->getPath(), true);
    splash->fill(&path, true);
}

void SplashOutputDev::clip(GfxState *state)
{
    const SplashPath path = convertPath(state->getPath(), true);
    splash->clipToPath(path, false);
}

void SplashOutputDev::eoClip(GfxState *state)
{
    const SplashPath path = convertPath(state->getPath(), true);
    splash->clipToPath(path, true);
}

void SplashOutputDev::clipToStrokePath(GfxState *state)
{
    const SplashPath path = convertPath(state->getPath(), false);
    const std::unique_ptr<SplashPath> path2 = splash->makeStrokePath(path, state->getLineWidth());
    splash->clipToPath(*path2, false);
}

SplashPath SplashOutputDev::convertPath(const GfxPath *path, bool dropEmptySubpaths)
{
    SplashPath sPath;
    int n, i, j;

    n = dropEmptySubpaths ? 1 : 0;
    for (i = 0; i < path->getNumSubpaths(); ++i) {
        const GfxSubpath *subpath = path->getSubpath(i);
        if (subpath->getNumPoints() > n) {
            sPath.reserve(subpath->getNumPoints() + 1);
            sPath.moveTo(subpath->getX(0), subpath->getY(0));
            j = 1;
            while (j < subpath->getNumPoints()) {
                if (subpath->getCurve(j)) {
                    sPath.curveTo(subpath->getX(j), subpath->getY(j), subpath->getX(j + 1), subpath->getY(j + 1), subpath->getX(j + 2), subpath->getY(j + 2));
                    j += 3;
                } else {
                    sPath.lineTo(subpath->getX(j), subpath->getY(j));
                    ++j;
                }
            }
            if (subpath->isClosed()) {
                sPath.close();
            }
        }
    }
    return sPath;
}

void SplashOutputDev::drawChar(GfxState *state, double x, double y, double /*dx*/, double /*dy*/, double originX, double originY, CharCode code, int /*nBytes*/, const Unicode * /*u*/, int /*uLen*/)
{
    SplashPath *path;
    int render;
    bool doFill, doStroke, doClip, strokeAdjust;
    double m[4];
    bool horiz;

    if (skipHorizText || skipRotatedText) {
        state->getFontTransMat(&m[0], &m[1], &m[2], &m[3]);
        horiz = m[0] > 0 && fabs(m[1]) < 0.001 && fabs(m[2]) < 0.001 && m[3] < 0;
        if ((skipHorizText && horiz) || (skipRotatedText && !horiz)) {
            return;
        }
    }

    // check for invisible text -- this is used by Acrobat Capture
    render = state->getRender();
    if (render == 3) {
        return;
    }

    if (needFontUpdate) {
        doUpdateFont(state);
    }
    if (!font) {
        return;
    }

    x -= originX;
    y -= originY;

    doFill = !(render & 1) && !state->getFillColorSpace()->isNonMarking();
    doStroke = ((render & 3) == 1 || (render & 3) == 2) && !state->getStrokeColorSpace()->isNonMarking();
    doClip = render & 4;

    path = nullptr;
    double lineWidth = splash->getLineWidth();
    if (doStroke && lineWidth == 0.0) {
        splash->setLineWidth(1 / state->getVDPI());
    }
    if (doStroke || doClip) {
        if ((path = font->getGlyphPath(code))) {
            path->offset(x, y);
        }
    }

    // don't use stroke adjustment when stroking text -- the results
    // tend to be ugly (because characters with horizontal upper or
    // lower edges get misaligned relative to the other characters)
    strokeAdjust = false; // make gcc happy
    if (doStroke) {
        strokeAdjust = splash->getStrokeAdjust();
        splash->setStrokeAdjust(false);
    }

    // fill and stroke
    if (doFill && doStroke) {
        if (path) {
            setOverprintMask(state->getFillColorSpace(), state->getFillOverprint(), state->getOverprintMode(), &state->getFillColor());
            splash->fill(path, false);
            setOverprintMask(state->getStrokeColorSpace(), state->getStrokeOverprint(), state->getOverprintMode(), &state->getStrokeColor());
            splash->stroke(*path);
        }

        // fill
    } else if (doFill) {
        setOverprintMask(state->getFillColorSpace(), state->getFillOverprint(), state->getOverprintMode(), &state->getFillColor());
        splash->fillChar(x, y, code, font);

        // stroke
    } else if (doStroke) {
        if (path) {
            setOverprintMask(state->getStrokeColorSpace(), state->getStrokeOverprint(), state->getOverprintMode(), &state->getStrokeColor());
            splash->stroke(*path);
        }
    }
    splash->setLineWidth(lineWidth);

    // clip
    if (doClip) {
        if (path) {
            if (textClipPath) {
                textClipPath->append(path);
            } else {
                textClipPath = path;
                path = nullptr;
            }
        }
    }

    if (doStroke) {
        splash->setStrokeAdjust(strokeAdjust);
    }

    delete path;
}

bool SplashOutputDev::beginType3Char(GfxState *state, double /*x*/, double /*y*/, double /*dx*/, double /*dy*/, CharCode code, const Unicode * /*u*/, int /*uLen*/)
{
    std::shared_ptr<const GfxFont> gfxFont;
    const Ref *fontID;
    T3FontCache *t3Font;
    T3GlyphStack *t3gs;
    bool validBBox;
    double m[4];
    bool horiz;
    double x1, y1, xMin, yMin, xMax, yMax, xt, yt;
    int i, j;

    // check for invisible text -- this is used by Acrobat Capture
    if (state->getRender() == 3) {
        // this is a bit of cheating, we say yes, font is already on cache
        // so we actually skip the rendering of it
        return true;
    }

    if (skipHorizText || skipRotatedText) {
        state->getFontTransMat(&m[0], &m[1], &m[2], &m[3]);
        horiz = m[0] > 0 && fabs(m[1]) < 0.001 && fabs(m[2]) < 0.001 && m[3] < 0;
        if ((skipHorizText && horiz) || (skipRotatedText && !horiz)) {
            return true;
        }
    }

    if (!(gfxFont = state->getFont())) {
        return false;
    }
    fontID = gfxFont->getID();
    const std::array<double, 6> &ctm = state->getCTM();
    state->transform(0, 0, &xt, &yt);

    // is it the first (MRU) font in the cache?
    if (nT3Fonts <= 0 || !t3FontCache[0]->matches(fontID, ctm[0], ctm[1], ctm[2], ctm[3])) {

        // is the font elsewhere in the cache?
        for (i = 1; i < nT3Fonts; ++i) {
            if (t3FontCache[i]->matches(fontID, ctm[0], ctm[1], ctm[2], ctm[3])) {
                t3Font = t3FontCache[i];
                for (j = i; j > 0; --j) {
                    t3FontCache[j] = t3FontCache[j - 1];
                }
                t3FontCache[0] = t3Font;
                break;
            }
        }
        if (i >= nT3Fonts) {

            // create new entry in the font cache
            if (nT3Fonts == splashOutT3FontCacheSize) {
                t3gs = t3GlyphStack;
                while (t3gs != nullptr) {
                    if (t3gs->cache == t3FontCache[nT3Fonts - 1]) {
                        error(errSyntaxWarning, -1, "t3FontCache reaches limit but font still on stack in SplashOutputDev::beginType3Char");
                        return true;
                    }
                    t3gs = t3gs->next;
                }
                delete t3FontCache[nT3Fonts - 1];
                --nT3Fonts;
            }
            for (j = nT3Fonts; j > 0; --j) {
                t3FontCache[j] = t3FontCache[j - 1];
            }
            ++nT3Fonts;
            const std::array<double, 4> &bbox = gfxFont->getFontBBox();
            if (bbox[0] == 0 && bbox[1] == 0 && bbox[2] == 0 && bbox[3] == 0) {
                // unspecified bounding box -- just take a guess
                xMin = xt - 5;
                xMax = xMin + 30;
                yMax = yt + 15;
                yMin = yMax - 45;
                validBBox = false;
            } else {
                state->transform(bbox[0], bbox[1], &x1, &y1);
                xMin = xMax = x1;
                yMin = yMax = y1;
                state->transform(bbox[0], bbox[3], &x1, &y1);
                if (x1 < xMin) {
                    xMin = x1;
                } else if (x1 > xMax) {
                    xMax = x1;
                }
                if (y1 < yMin) {
                    yMin = y1;
                } else if (y1 > yMax) {
                    yMax = y1;
                }
                state->transform(bbox[2], bbox[1], &x1, &y1);
                if (x1 < xMin) {
                    xMin = x1;
                } else if (x1 > xMax) {
                    xMax = x1;
                }
                if (y1 < yMin) {
                    yMin = y1;
                } else if (y1 > yMax) {
                    yMax = y1;
                }
                state->transform(bbox[2], bbox[3], &x1, &y1);
                if (x1 < xMin) {
                    xMin = x1;
                } else if (x1 > xMax) {
                    xMax = x1;
                }
                if (y1 < yMin) {
                    yMin = y1;
                } else if (y1 > yMax) {
                    yMax = y1;
                }
                validBBox = true;
            }
            t3FontCache[0] = new T3FontCache(fontID, ctm[0], ctm[1], ctm[2], ctm[3], static_cast<int>(floor(xMin - xt)) - 2, static_cast<int>(floor(yMin - yt)) - 2, static_cast<int>(ceil(xMax)) - static_cast<int>(floor(xMin)) + 4,
                                             static_cast<int>(ceil(yMax)) - static_cast<int>(floor(yMin)) + 4, validBBox, colorMode != splashModeMono1);
        }
    }
    t3Font = t3FontCache[0];

    // is the glyph in the cache?
    i = (code & (t3Font->cacheSets - 1)) * t3Font->cacheAssoc;
    for (j = 0; j < t3Font->cacheAssoc; ++j) {
        if (t3Font->cacheTags != nullptr) {
            if ((t3Font->cacheTags[i + j].mru & 0x8000) && t3Font->cacheTags[i + j].code == code) {
                drawType3Glyph(state, t3Font, &t3Font->cacheTags[i + j], t3Font->cacheData + (i + j) * t3Font->glyphSize);
                return true;
            }
        }
    }

    // push a new Type 3 glyph record
    t3gs = new T3GlyphStack();
    t3gs->next = t3GlyphStack;
    t3GlyphStack = t3gs;
    t3GlyphStack->code = code;
    t3GlyphStack->cache = t3Font;
    t3GlyphStack->cacheTag = nullptr;
    t3GlyphStack->cacheData = nullptr;
    t3GlyphStack->haveDx = false;
    t3GlyphStack->doNotCache = false;

    return false;
}

void SplashOutputDev::endType3Char(GfxState *state)
{
    T3GlyphStack *t3gs;

    if (t3GlyphStack->cacheTag) {
        memcpy(t3GlyphStack->cacheData, bitmap->getDataPtr(), t3GlyphStack->cache->glyphSize);
        delete bitmap;
        delete splash;
        bitmap = t3GlyphStack->origBitmap;
        splash = t3GlyphStack->origSplash;
        const std::array<double, 6> &ctm = state->getCTM();
        state->setCTM(ctm[0], ctm[1], ctm[2], ctm[3], t3GlyphStack->origCTM4, t3GlyphStack->origCTM5);
        updateCTM(state, 0, 0, 0, 0, 0, 0);
        drawType3Glyph(state, t3GlyphStack->cache, t3GlyphStack->cacheTag, t3GlyphStack->cacheData);
    }
    t3gs = t3GlyphStack;
    t3GlyphStack = t3gs->next;
    delete t3gs;
}

void SplashOutputDev::type3D0(GfxState * /*state*/, double /*wx*/, double /*wy*/)
{
    if (likely(t3GlyphStack != nullptr)) {
        t3GlyphStack->haveDx = true;
    } else {
        error(errSyntaxWarning, -1, "t3GlyphStack was null in SplashOutputDev::type3D0");
    }
}

void SplashOutputDev::type3D1(GfxState *state, double /*wx*/, double /*wy*/, double llx, double lly, double urx, double ury)
{
    T3FontCache *t3Font;
    SplashColor color;
    double xt, yt, xMin, xMax, yMin, yMax, x1, y1;
    int i, j;

    // ignore multiple d0/d1 operators
    if (!t3GlyphStack || t3GlyphStack->haveDx) {
        return;
    }
    t3GlyphStack->haveDx = true;
    // don't cache if we got a gsave/grestore before the d1
    if (t3GlyphStack->doNotCache) {
        return;
    }

    if (unlikely(t3GlyphStack == nullptr)) {
        error(errSyntaxWarning, -1, "t3GlyphStack was null in SplashOutputDev::type3D1");
        return;
    }

    if (unlikely(t3GlyphStack->origBitmap != nullptr)) {
        error(errSyntaxWarning, -1, "t3GlyphStack origBitmap was not null in SplashOutputDev::type3D1");
        return;
    }

    if (unlikely(t3GlyphStack->origSplash != nullptr)) {
        error(errSyntaxWarning, -1, "t3GlyphStack origSplash was not null in SplashOutputDev::type3D1");
        return;
    }

    t3Font = t3GlyphStack->cache;

    // check for a valid bbox
    state->transform(0, 0, &xt, &yt);
    state->transform(llx, lly, &x1, &y1);
    xMin = xMax = x1;
    yMin = yMax = y1;
    state->transform(llx, ury, &x1, &y1);
    if (x1 < xMin) {
        xMin = x1;
    } else if (x1 > xMax) {
        xMax = x1;
    }
    if (y1 < yMin) {
        yMin = y1;
    } else if (y1 > yMax) {
        yMax = y1;
    }
    state->transform(urx, lly, &x1, &y1);
    if (x1 < xMin) {
        xMin = x1;
    } else if (x1 > xMax) {
        xMax = x1;
    }
    if (y1 < yMin) {
        yMin = y1;
    } else if (y1 > yMax) {
        yMax = y1;
    }
    state->transform(urx, ury, &x1, &y1);
    if (x1 < xMin) {
        xMin = x1;
    } else if (x1 > xMax) {
        xMax = x1;
    }
    if (y1 < yMin) {
        yMin = y1;
    } else if (y1 > yMax) {
        yMax = y1;
    }
    if (xMin - xt < t3Font->glyphX || yMin - yt < t3Font->glyphY || xMax - xt > t3Font->glyphX + t3Font->glyphW || yMax - yt > t3Font->glyphY + t3Font->glyphH) {
        if (t3Font->validBBox) {
            error(errSyntaxWarning, -1, "Bad bounding box in Type 3 glyph");
        }
        return;
    }

    if (t3Font->cacheTags == nullptr) {
        return;
    }

    // allocate a cache entry
    i = (t3GlyphStack->code & (t3Font->cacheSets - 1)) * t3Font->cacheAssoc;
    for (j = 0; j < t3Font->cacheAssoc; ++j) {
        if ((t3Font->cacheTags[i + j].mru & 0x7fff) == t3Font->cacheAssoc - 1) {
            t3Font->cacheTags[i + j].mru = 0x8000;
            t3Font->cacheTags[i + j].code = t3GlyphStack->code;
            t3GlyphStack->cacheTag = &t3Font->cacheTags[i + j];
            t3GlyphStack->cacheData = t3Font->cacheData + (i + j) * t3Font->glyphSize;
        } else {
            ++t3Font->cacheTags[i + j].mru;
        }
    }

    // save state
    t3GlyphStack->origBitmap = bitmap;
    t3GlyphStack->origSplash = splash;
    const std::array<double, 6> &ctm = state->getCTM();
    t3GlyphStack->origCTM4 = ctm[4];
    t3GlyphStack->origCTM5 = ctm[5];

    // create the temporary bitmap
    if (colorMode == splashModeMono1) {
        bitmap = new SplashBitmap(t3Font->glyphW, t3Font->glyphH, 1, splashModeMono1, false);
        splash = new Splash(bitmap, false, t3GlyphStack->origSplash->getScreen());
        color[0] = 0;
        splash->clear(color);
        color[0] = 0xff;
    } else {
        bitmap = new SplashBitmap(t3Font->glyphW, t3Font->glyphH, 1, splashModeMono8, false);
        splash = new Splash(bitmap, vectorAntialias, t3GlyphStack->origSplash->getScreen());
        color[0] = 0x00;
        splash->clear(color);
        color[0] = 0xff;
    }
    splash->setMinLineWidth(s_minLineWidth);
    splash->setThinLineMode(splashThinLineDefault);
    splash->setFillPattern(new SplashSolidColor(color));
    splash->setStrokePattern(new SplashSolidColor(color));
    //~ this should copy other state from t3GlyphStack->origSplash?
    state->setCTM(ctm[0], ctm[1], ctm[2], ctm[3], -t3Font->glyphX, -t3Font->glyphY);
    updateCTM(state, 0, 0, 0, 0, 0, 0);
}

void SplashOutputDev::drawType3Glyph(GfxState *state, T3FontCache *t3Font, T3FontCacheTag * /*tag*/, unsigned char *data)
{
    SplashGlyphBitmap glyph;

    setOverprintMask(state->getFillColorSpace(), state->getFillOverprint(), state->getOverprintMode(), &state->getFillColor());
    glyph.x = -t3Font->glyphX;
    glyph.y = -t3Font->glyphY;
    glyph.w = t3Font->glyphW;
    glyph.h = t3Font->glyphH;
    glyph.aa = colorMode != splashModeMono1;
    glyph.data = data;
    glyph.freeData = false;
    splash->fillGlyph(0, 0, &glyph);
}

void SplashOutputDev::beginTextObject(GfxState * /*state*/) { }

void SplashOutputDev::endTextObject(GfxState * /*state*/)
{
    if (textClipPath) {
        splash->clipToPath(*textClipPath, false);
        delete textClipPath;
        textClipPath = nullptr;
    }
}

struct SplashOutImageMaskData
{
    std::unique_ptr<ImageStream> imgStr;
    bool invert;
    int width, height, y;
};

bool SplashOutputDev::imageMaskSrc(void *data, SplashColorPtr line)
{
    auto *imgMaskData = static_cast<SplashOutImageMaskData *>(data);
    unsigned char *p;
    SplashColorPtr q;
    int x;

    if (imgMaskData->y == imgMaskData->height) {
        return false;
    }
    if (!(p = imgMaskData->imgStr->getLine())) {
        return false;
    }
    for (x = 0, q = line; x < imgMaskData->width; ++x) {
        *q++ = *p++ ^ imgMaskData->invert;
    }
    ++imgMaskData->y;
    return true;
}

void SplashOutputDev::drawImageMask(GfxState *state, Object * /*ref*/, Stream *str, int width, int height, bool invert, bool /*interpolate*/, bool inlineImg)
{
    std::array<double, 6> mat;
    SplashOutImageMaskData imgMaskData;

    if (state->getFillColorSpace()->isNonMarking()) {
        return;
    }
    setOverprintMask(state->getFillColorSpace(), state->getFillOverprint(), state->getOverprintMode(), &state->getFillColor());

    const std::array<double, 6> &ctm = state->getCTM();
    for (int i = 0; i < 6; ++i) {
        if (!std::isfinite(ctm[i])) {
            return;
        }
    }
    mat[0] = ctm[0];
    mat[1] = ctm[1];
    mat[2] = -ctm[2];
    mat[3] = -ctm[3];
    mat[4] = ctm[2] + ctm[4];
    mat[5] = ctm[3] + ctm[5];

    imgMaskData.imgStr = std::make_unique<ImageStream>(str, width, 1, 1);
    if (!imgMaskData.imgStr->rewind()) {
        return;
    }
    imgMaskData.invert = !invert;
    imgMaskData.width = width;
    imgMaskData.height = height;
    imgMaskData.y = 0;

    splash->fillImageMask(&imageMaskSrc, &imgMaskData, width, height, mat, t3GlyphStack != nullptr);
    if (inlineImg) {
        while (imgMaskData.y < height) {
            if (!imgMaskData.imgStr->getLine()) {
                break;
            }
            ++imgMaskData.y;
        }
    }

    str->close();
}

bool SplashOutputDev::setSoftMaskFromImageMask(GfxState *state, Object * /*ref*/, Stream *str, int width, int height, bool invert, bool /*inlineImg*/, std::array<double, 6> &baseMatrix)
{
    std::array<double, 6> mat;
    SplashOutImageMaskData imgMaskData;
    Splash *maskSplash;
    SplashColor maskColor;
    static constexpr std::array<double, 4> bbox = { 0, 0, 1, 1 }; // default;

    if (state->getFillColorSpace()->isNonMarking()) {
        return false;
    }

    const std::array<double, 6> &ctm = state->getCTM();
    for (int i = 0; i < 6; ++i) {
        if (!std::isfinite(ctm[i])) {
            return false;
        }
    }
    imgMaskData.imgStr = std::make_unique<ImageStream>(str, width, 1, 1);
    if (!imgMaskData.imgStr->rewind()) {
        return false;
    }

    beginTransparencyGroup(state, bbox, nullptr, false, false, false);
    baseMatrix[4] -= transpGroupStack->tx;
    baseMatrix[5] -= transpGroupStack->ty;

    mat[0] = ctm[0];
    mat[1] = ctm[1];
    mat[2] = -ctm[2];
    mat[3] = -ctm[3];
    mat[4] = ctm[2] + ctm[4];
    mat[5] = ctm[3] + ctm[5];
    imgMaskData.invert = !invert;
    imgMaskData.width = width;
    imgMaskData.height = height;
    imgMaskData.y = 0;

    transpGroupStack->softmask = new SplashBitmap(bitmap->getWidth(), bitmap->getHeight(), 1, splashModeMono8, false);
    maskSplash = new Splash(transpGroupStack->softmask, vectorAntialias);
    maskColor[0] = 0;
    maskSplash->clear(maskColor);
    maskColor[0] = 0xff;
    maskSplash->setFillPattern(new SplashSolidColor(maskColor));
    maskSplash->fillImageMask(&imageMaskSrc, &imgMaskData, width, height, mat, t3GlyphStack != nullptr);
    delete maskSplash;
    str->close();

    return true;
}

void SplashOutputDev::unsetSoftMaskFromImageMask(GfxState *state, std::array<double, 6> &baseMatrix)
{
    static constexpr std::array<double, 4> bbox = { 0, 0, 1, 1 }; // dummy

    if (!transpGroupStack) {
        return;
    }

    /* transfer mask to alpha channel! */
    // memcpy(maskBitmap->getAlphaPtr(), maskBitmap->getDataPtr(), bitmap->getRowSize() * bitmap->getHeight());
    // memset(maskBitmap->getDataPtr(), 0, bitmap->getRowSize() * bitmap->getHeight());
    if (transpGroupStack->softmask != nullptr) {
        unsigned char *dest = bitmap->getAlphaPtr();
        const unsigned char *src = transpGroupStack->softmask->getDataPtr();
        const int nPixels = std::min(bitmap->getRowSize() * bitmap->getHeight(), transpGroupStack->softmask->getRowSize() * transpGroupStack->softmask->getHeight());
        for (int c = 0; c < nPixels; c++) {
            dest[c] = src[c];
        }
        delete transpGroupStack->softmask;
        transpGroupStack->softmask = nullptr;
    }
    endTransparencyGroup(state);
    baseMatrix[4] += transpGroupStack->tx;
    baseMatrix[5] += transpGroupStack->ty;
    paintTransparencyGroup(state, bbox);
}

struct SplashOutImageData
{
    std::unique_ptr<ImageStream> imgStr;
    GfxImageColorMap *colorMap;
    SplashColorPtr lookup;
    const int *maskColors;
    SplashColorMode colorMode;
    int width, height, y;
    std::unique_ptr<ImageStream> maskStr;
    GfxImageColorMap *maskColorMap;
    SplashColor matteColor;
};

#if USE_CMS
bool SplashOutputDev::useIccImageSrc(void *data)
{
    auto *imgData = static_cast<SplashOutImageData *>(data);

    if (!imgData->lookup && imgData->colorMap->getColorSpace()->getMode() == csICCBased && imgData->colorMap->getBits() != 1) {
        auto *colorSpace = static_cast<GfxICCBasedColorSpace *>(imgData->colorMap->getColorSpace());
        switch (imgData->colorMode) {
        case splashModeMono1:
        case splashModeMono8:
            if (colorSpace->getAlt() != nullptr && colorSpace->getAlt()->getMode() == csDeviceGray) {
                return true;
            }
            break;
        case splashModeXBGR8:
        case splashModeRGB8:
        case splashModeBGR8:
            if (colorSpace->getAlt() != nullptr && colorSpace->getAlt()->getMode() == csDeviceRGB) {
                return true;
            }
            break;
        case splashModeCMYK8:
            if (colorSpace->getAlt() != nullptr && colorSpace->getAlt()->getMode() == csDeviceCMYK) {
                return true;
            }
            break;
        case splashModeDeviceN8:
            if (colorSpace->getAlt() != nullptr && colorSpace->getAlt()->getMode() == csDeviceN) {
                return true;
            }
            break;
        }
    }

    return false;
}
#endif

// Clip x to lie in [0, 255].
static inline unsigned char clip255(int x)
{
    return x < 0 ? 0 : x > 255 ? 255 : x;
}

bool SplashOutputDev::imageSrc(void *data, SplashColorPtr colorLine, unsigned char * /*alphaLine*/)
{
    auto *imgData = static_cast<SplashOutImageData *>(data);
    unsigned char *p;
    SplashColorPtr q, col;
    GfxRGB rgb;
    GfxGray gray;
    GfxCMYK cmyk;
    GfxColor deviceN;
    int nComps, x;

    if (imgData->y == imgData->height) {
        return false;
    }
    if (!(p = imgData->imgStr->getLine())) {
        int destComps = 1;
        if (imgData->colorMode == splashModeRGB8 || imgData->colorMode == splashModeBGR8) {
            destComps = 3;
        } else if (imgData->colorMode == splashModeXBGR8) {
            destComps = 4;
        } else if (imgData->colorMode == splashModeCMYK8) {
            destComps = 4;
        } else if (imgData->colorMode == splashModeDeviceN8) {
            destComps = SPOT_NCOMPS + 4;
        }
        memset(colorLine, 0, imgData->width * destComps);
        return false;
    }

    nComps = imgData->colorMap->getNumPixelComps();

    if (imgData->lookup) {
        switch (imgData->colorMode) {
        case splashModeMono1:
        case splashModeMono8:
            for (x = 0, q = colorLine; x < imgData->width; ++x, ++p) {
                *q++ = imgData->lookup[*p];
            }
            break;
        case splashModeRGB8:
        case splashModeBGR8:
            for (x = 0, q = colorLine; x < imgData->width; ++x, ++p) {
                col = &imgData->lookup[3 * *p];
                *q++ = col[0];
                *q++ = col[1];
                *q++ = col[2];
            }
            break;
        case splashModeXBGR8:
            for (x = 0, q = colorLine; x < imgData->width; ++x, ++p) {
                col = &imgData->lookup[4 * *p];
                *q++ = col[0];
                *q++ = col[1];
                *q++ = col[2];
                *q++ = col[3];
            }
            break;
        case splashModeCMYK8:
            for (x = 0, q = colorLine; x < imgData->width; ++x, ++p) {
                col = &imgData->lookup[4 * *p];
                *q++ = col[0];
                *q++ = col[1];
                *q++ = col[2];
                *q++ = col[3];
            }
            break;
        case splashModeDeviceN8:
            for (x = 0, q = colorLine; x < imgData->width; ++x, ++p) {
                col = &imgData->lookup[(SPOT_NCOMPS + 4) * *p];
                for (int cp = 0; cp < SPOT_NCOMPS + 4; cp++) {
                    *q++ = col[cp];
                }
            }
            break;
        }
    } else {
        switch (imgData->colorMode) {
        case splashModeMono1:
        case splashModeMono8:
            for (x = 0, q = colorLine; x < imgData->width; ++x, p += nComps) {
                imgData->colorMap->getGray(p, &gray);
                *q++ = colToByte(gray);
            }
            break;
        case splashModeRGB8:
        case splashModeBGR8:
            if (imgData->colorMap->useRGBLine()) {
                imgData->colorMap->getRGBLine(p, static_cast<unsigned char *>(colorLine), imgData->width);
            } else {
                for (x = 0, q = colorLine; x < imgData->width; ++x, p += nComps) {
                    imgData->colorMap->getRGB(p, &rgb);
                    *q++ = colToByte(rgb.r);
                    *q++ = colToByte(rgb.g);
                    *q++ = colToByte(rgb.b);
                }
            }
            break;
        case splashModeXBGR8:
            if (imgData->colorMap->useRGBLine()) {
                imgData->colorMap->getRGBXLine(p, static_cast<unsigned char *>(colorLine), imgData->width);
            } else {
                for (x = 0, q = colorLine; x < imgData->width; ++x, p += nComps) {
                    imgData->colorMap->getRGB(p, &rgb);
                    *q++ = colToByte(rgb.r);
                    *q++ = colToByte(rgb.g);
                    *q++ = colToByte(rgb.b);
                    *q++ = 255;
                }
            }
            break;
        case splashModeCMYK8:
            if (imgData->colorMap->useCMYKLine()) {
                imgData->colorMap->getCMYKLine(p, static_cast<unsigned char *>(colorLine), imgData->width);
            } else {
                for (x = 0, q = colorLine; x < imgData->width; ++x, p += nComps) {
                    imgData->colorMap->getCMYK(p, &cmyk);
                    *q++ = colToByte(cmyk.c);
                    *q++ = colToByte(cmyk.m);
                    *q++ = colToByte(cmyk.y);
                    *q++ = colToByte(cmyk.k);
                }
            }
            break;
        case splashModeDeviceN8:
            if (imgData->colorMap->useDeviceNLine()) {
                imgData->colorMap->getDeviceNLine(p, static_cast<unsigned char *>(colorLine), imgData->width);
            } else {
                for (x = 0, q = colorLine; x < imgData->width; ++x, p += nComps) {
                    imgData->colorMap->getDeviceN(p, &deviceN);
                    for (int cp = 0; cp < SPOT_NCOMPS + 4; cp++) {
                        *q++ = colToByte(deviceN.c[cp]);
                    }
                }
            }
            break;
        }
    }

    if (imgData->maskStr && (p = imgData->maskStr->getLine()) != nullptr) {
        int destComps = splashColorModeNComps[imgData->colorMode];
        int convComps = (imgData->colorMode == splashModeXBGR8) ? 3 : destComps;
        imgData->maskColorMap->getGrayLine(p, p, imgData->width);
        for (x = 0, q = colorLine; x < imgData->width; ++x, p++, q += destComps) {
            for (int cp = 0; cp < convComps; cp++) {
                q[cp] = (*p) ? clip255(imgData->matteColor[cp] + (q[cp] - imgData->matteColor[cp]) * 255 / *p) : imgData->matteColor[cp];
            }
        }
    }
    ++imgData->y;
    return true;
}

#if USE_CMS
bool SplashOutputDev::iccImageSrc(void *data, SplashColorPtr colorLine, unsigned char * /*alphaLine*/)
{
    auto *imgData = static_cast<SplashOutImageData *>(data);
    unsigned char *p;
    int nComps;

    if (imgData->y == imgData->height) {
        return false;
    }
    if (!(p = imgData->imgStr->getLine())) {
        int destComps = 1;
        if (imgData->colorMode == splashModeRGB8 || imgData->colorMode == splashModeBGR8) {
            destComps = 3;
        } else if (imgData->colorMode == splashModeXBGR8) {
            destComps = 4;
        } else if (imgData->colorMode == splashModeCMYK8) {
            destComps = 4;
        } else if (imgData->colorMode == splashModeDeviceN8) {
            destComps = SPOT_NCOMPS + 4;
        }
        memset(colorLine, 0, imgData->width * destComps);
        return false;
    }

    if (imgData->colorMode == splashModeXBGR8) {
        SplashColorPtr q;
        int x;
        for (x = 0, q = colorLine; x < imgData->width; ++x) {
            *q++ = *p++;
            *q++ = *p++;
            *q++ = *p++;
            *q++ = 255;
        }
    } else {
        nComps = imgData->colorMap->getNumPixelComps();
        memcpy(colorLine, p, imgData->width * nComps);
    }

    ++imgData->y;
    return true;
}

void SplashOutputDev::iccTransform(void *data, SplashBitmap *bitmap)
{
    auto *imgData = static_cast<SplashOutImageData *>(data);
    int nComps = imgData->colorMap->getNumPixelComps();

    auto *colorLine = static_cast<unsigned char *>(gmalloc(nComps * bitmap->getWidth()));
    unsigned char *rgbxLine = (imgData->colorMode == splashModeXBGR8) ? static_cast<unsigned char *>(gmalloc(3 * bitmap->getWidth())) : nullptr;
    for (int i = 0; i < bitmap->getHeight(); i++) {
        unsigned char *p = bitmap->getDataPtr() + i * bitmap->getRowSize();
        switch (imgData->colorMode) {
        case splashModeMono1:
        case splashModeMono8:
            imgData->colorMap->getGrayLine(p, colorLine, bitmap->getWidth());
            memcpy(p, colorLine, nComps * bitmap->getWidth());
            break;
        case splashModeRGB8:
        case splashModeBGR8:
            imgData->colorMap->getRGBLine(p, colorLine, bitmap->getWidth());
            memcpy(p, colorLine, nComps * bitmap->getWidth());
            break;
        case splashModeCMYK8:
            imgData->colorMap->getCMYKLine(p, colorLine, bitmap->getWidth());
            memcpy(p, colorLine, nComps * bitmap->getWidth());
            break;
        case splashModeDeviceN8:
            imgData->colorMap->getDeviceNLine(p, colorLine, bitmap->getWidth());
            memcpy(p, colorLine, nComps * bitmap->getWidth());
            break;
        case splashModeXBGR8:
            unsigned char *q;
            unsigned char *b = p;
            int x;
            for (x = 0, q = rgbxLine; x < bitmap->getWidth(); ++x, b += 4) {
                *q++ = b[2];
                *q++ = b[1];
                *q++ = b[0];
            }
            imgData->colorMap->getRGBLine(rgbxLine, colorLine, bitmap->getWidth());
            b = p;
            for (x = 0, q = colorLine; x < bitmap->getWidth(); ++x, b += 4) {
                b[2] = *q++;
                b[1] = *q++;
                b[0] = *q++;
            }
            break;
        }
    }
    gfree(colorLine);
    if (rgbxLine != nullptr) {
        gfree(rgbxLine);
    }
}
#endif

bool SplashOutputDev::alphaImageSrc(void *data, SplashColorPtr colorLine, unsigned char *alphaLine)
{
    auto *imgData = static_cast<SplashOutImageData *>(data);
    unsigned char *p, *aq;
    SplashColorPtr q, col;
    GfxRGB rgb;
    GfxGray gray;
    GfxCMYK cmyk;
    GfxColor deviceN;
    unsigned char alpha;
    int nComps, x, i;

    if (imgData->y == imgData->height) {
        return false;
    }
    if (!(p = imgData->imgStr->getLine())) {
        return false;
    }

    nComps = imgData->colorMap->getNumPixelComps();

    for (x = 0, q = colorLine, aq = alphaLine; x < imgData->width; ++x, p += nComps) {
        alpha = 0;
        for (i = 0; i < nComps; ++i) {
            if (p[i] < imgData->maskColors[2 * i] || p[i] > imgData->maskColors[2 * i + 1]) {
                alpha = 0xff;
                break;
            }
        }
        if (imgData->lookup) {
            switch (imgData->colorMode) {
            case splashModeMono1:
            case splashModeMono8:
                *q++ = imgData->lookup[*p];
                break;
            case splashModeRGB8:
            case splashModeBGR8:
                col = &imgData->lookup[3 * *p];
                *q++ = col[0];
                *q++ = col[1];
                *q++ = col[2];
                break;
            case splashModeXBGR8:
                col = &imgData->lookup[4 * *p];
                *q++ = col[0];
                *q++ = col[1];
                *q++ = col[2];
                *q++ = 255;
                break;
            case splashModeCMYK8:
                col = &imgData->lookup[4 * *p];
                *q++ = col[0];
                *q++ = col[1];
                *q++ = col[2];
                *q++ = col[3];
                break;
            case splashModeDeviceN8:
                col = &imgData->lookup[(SPOT_NCOMPS + 4) * *p];
                for (int cp = 0; cp < SPOT_NCOMPS + 4; cp++) {
                    *q++ = col[cp];
                }
                break;
            }
            *aq++ = alpha;
        } else {
            switch (imgData->colorMode) {
            case splashModeMono1:
            case splashModeMono8:
                imgData->colorMap->getGray(p, &gray);
                *q++ = colToByte(gray);
                break;
            case splashModeXBGR8:
            case splashModeRGB8:
            case splashModeBGR8:
                imgData->colorMap->getRGB(p, &rgb);
                *q++ = colToByte(rgb.r);
                *q++ = colToByte(rgb.g);
                *q++ = colToByte(rgb.b);
                if (imgData->colorMode == splashModeXBGR8) {
                    *q++ = 255;
                }
                break;
            case splashModeCMYK8:
                imgData->colorMap->getCMYK(p, &cmyk);
                *q++ = colToByte(cmyk.c);
                *q++ = colToByte(cmyk.m);
                *q++ = colToByte(cmyk.y);
                *q++ = colToByte(cmyk.k);
                break;
            case splashModeDeviceN8:
                imgData->colorMap->getDeviceN(p, &deviceN);
                for (int cp = 0; cp < SPOT_NCOMPS + 4; cp++) {
                    *q++ = colToByte(deviceN.c[cp]);
                }
                break;
            }
            *aq++ = alpha;
        }
    }

    ++imgData->y;
    return true;
}

struct TilingSplashOutBitmap
{
    SplashBitmap *bitmap;
    SplashPattern *pattern;
    SplashColorMode colorMode;
    int paintType;
    int repeatX;
    int repeatY;
    int y;
};

bool SplashOutputDev::tilingBitmapSrc(void *data, SplashColorPtr colorLine, unsigned char *alphaLine)
{
    auto *imgData = static_cast<TilingSplashOutBitmap *>(data);

    if (imgData->y == imgData->bitmap->getHeight()) {
        imgData->repeatY--;
        if (imgData->repeatY == 0) {
            return false;
        }
        imgData->y = 0;
    }

    if (imgData->paintType == 1) {
        const SplashColorMode cMode = imgData->bitmap->getMode();
        SplashColorPtr q = colorLine;
        // For splashModeBGR8 and splashModeXBGR8 we need to use getPixel
        // for the others we can use raw access
        if (cMode == splashModeBGR8 || cMode == splashModeXBGR8) {
            for (int m = 0; m < imgData->repeatX; m++) {
                for (int x = 0; x < imgData->bitmap->getWidth(); x++) {
                    imgData->bitmap->getPixel(x, imgData->y, q);
                    q += splashColorModeNComps[cMode];
                }
            }
        } else {
            const int n = imgData->bitmap->getRowSize();
            SplashColorConstPtr p;
            for (int m = 0; m < imgData->repeatX; m++) {
                p = imgData->bitmap->getDataPtr() + imgData->y * imgData->bitmap->getRowSize();
                for (int x = 0; x < n; ++x) {
                    *q++ = *p++;
                }
            }
        }
        if (alphaLine != nullptr) {
            SplashColorPtr aq = alphaLine;
            SplashColorPtr p;
            const int n = imgData->bitmap->getWidth() - 1;
            for (int m = 0; m < imgData->repeatX; m++) {
                p = imgData->bitmap->getAlphaPtr() + imgData->y * imgData->bitmap->getWidth();
                for (int x = 0; x < n; ++x) {
                    *aq++ = *p++;
                }
                // This is a hack, because of how Splash antialias works if we overwrite the
                // last alpha pixel of the tile most/all of the files look much better
                *aq++ = (n == 0) ? *p : *(p - 1);
            }
        }
    } else {
        SplashColor col, pat;
        SplashColorPtr dest = colorLine;
        for (int m = 0; m < imgData->repeatX; m++) {
            for (int x = 0; x < imgData->bitmap->getWidth(); x++) {
                imgData->bitmap->getPixel(x, imgData->y, col);
                imgData->pattern->getColor(x, imgData->y, pat);
                for (int i = 0; i < splashColorModeNComps[imgData->colorMode]; ++i) {
                    if (imgData->colorMode == splashModeCMYK8 || imgData->colorMode == splashModeDeviceN8) {
                        dest[i] = div255(pat[i] * (255 - col[0]));
                    } else {
                        dest[i] = 255 - div255((255 - pat[i]) * (255 - col[0]));
                    }
                }
                dest += splashColorModeNComps[imgData->colorMode];
            }
        }
        if (alphaLine != nullptr) {
            const int y = (imgData->y == imgData->bitmap->getHeight() - 1 && imgData->y > 50) ? imgData->y - 1 : imgData->y;
            SplashColorPtr aq = alphaLine;
            SplashColorPtr p;
            const int n = imgData->bitmap->getWidth();
            for (int m = 0; m < imgData->repeatX; m++) {
                p = imgData->bitmap->getAlphaPtr() + y * imgData->bitmap->getWidth();
                for (int x = 0; x < n; ++x) {
                    *aq++ = *p++;
                }
            }
        }
    }
    ++imgData->y;
    return true;
}

void SplashOutputDev::drawImage(GfxState *state, Object * /*ref*/, Stream *str, int width, int height, GfxImageColorMap *colorMap, bool interpolate, const int *maskColors, bool inlineImg)
{
    std::array<double, 6> mat;
    SplashOutImageData imgData;
    SplashColorMode srcMode;
    SplashImageSource src;
    SplashICCTransform tf;
    GfxGray gray;
    GfxRGB rgb;
    GfxCMYK cmyk;
    bool grayIndexed = false;
    GfxColor deviceN;
    unsigned char pix;
    int n, i;

    const std::array<double, 6> &ctm = state->getCTM();
    for (i = 0; i < 6; ++i) {
        if (!std::isfinite(ctm[i])) {
            return;
        }
    }
    imgData.imgStr = std::make_unique<ImageStream>(str, width, colorMap->getNumPixelComps(), colorMap->getBits());
    if (!imgData.imgStr->rewind()) {
        return;
    }

    mat[0] = ctm[0];
    mat[1] = ctm[1];
    mat[2] = -ctm[2];
    mat[3] = -ctm[3];
    mat[4] = ctm[2] + ctm[4];
    mat[5] = ctm[3] + ctm[5];

    imgData.colorMap = colorMap;
    imgData.maskColors = maskColors;
    imgData.colorMode = colorMode;
    imgData.width = width;
    imgData.height = height;
    imgData.maskColorMap = nullptr;
    imgData.y = 0;

    // special case for one-channel (monochrome/gray/separation) images:
    // build a lookup table here
    imgData.lookup = nullptr;
    if (colorMap->getNumPixelComps() == 1) {
        n = 1 << colorMap->getBits();
        switch (colorMode) {
        case splashModeMono1:
        case splashModeMono8:
            imgData.lookup = static_cast<SplashColorPtr>(gmalloc_checkoverflow(n));
            if (likely(imgData.lookup != nullptr)) {
                for (i = 0; i < n; ++i) {
                    pix = static_cast<unsigned char>(i);
                    colorMap->getGray(&pix, &gray);
                    imgData.lookup[i] = colToByte(gray);
                }
            }
            break;
        case splashModeRGB8:
        case splashModeBGR8:
            imgData.lookup = static_cast<SplashColorPtr>(gmallocn_checkoverflow(n, 3));
            if (likely(imgData.lookup != nullptr)) {
                for (i = 0; i < n; ++i) {
                    pix = static_cast<unsigned char>(i);
                    colorMap->getRGB(&pix, &rgb);
                    imgData.lookup[3 * i] = colToByte(rgb.r);
                    imgData.lookup[3 * i + 1] = colToByte(rgb.g);
                    imgData.lookup[3 * i + 2] = colToByte(rgb.b);
                }
            }
            break;
        case splashModeXBGR8:
            imgData.lookup = static_cast<SplashColorPtr>(gmallocn_checkoverflow(n, 4));
            if (likely(imgData.lookup != nullptr)) {
                for (i = 0; i < n; ++i) {
                    pix = static_cast<unsigned char>(i);
                    colorMap->getRGB(&pix, &rgb);
                    imgData.lookup[4 * i] = colToByte(rgb.r);
                    imgData.lookup[4 * i + 1] = colToByte(rgb.g);
                    imgData.lookup[4 * i + 2] = colToByte(rgb.b);
                    imgData.lookup[4 * i + 3] = 255;
                }
            }
            break;
        case splashModeCMYK8:
            grayIndexed = colorMap->getColorSpace()->getMode() != csDeviceGray;
            imgData.lookup = static_cast<SplashColorPtr>(gmallocn_checkoverflow(n, 4));
            if (likely(imgData.lookup != nullptr)) {
                for (i = 0; i < n; ++i) {
                    pix = static_cast<unsigned char>(i);
                    colorMap->getCMYK(&pix, &cmyk);
                    if (cmyk.c != 0 || cmyk.m != 0 || cmyk.y != 0) {
                        grayIndexed = false;
                    }
                    imgData.lookup[4 * i] = colToByte(cmyk.c);
                    imgData.lookup[4 * i + 1] = colToByte(cmyk.m);
                    imgData.lookup[4 * i + 2] = colToByte(cmyk.y);
                    imgData.lookup[4 * i + 3] = colToByte(cmyk.k);
                }
            }
            break;
        case splashModeDeviceN8:
            colorMap->getColorSpace()->createMapping(bitmap->getSeparationList(), SPOT_NCOMPS);
            grayIndexed = colorMap->getColorSpace()->getMode() != csDeviceGray;
            imgData.lookup = static_cast<SplashColorPtr>(gmallocn_checkoverflow(n, SPOT_NCOMPS + 4));
            if (likely(imgData.lookup != nullptr)) {
                for (i = 0; i < n; ++i) {
                    pix = static_cast<unsigned char>(i);
                    colorMap->getCMYK(&pix, &cmyk);
                    if (cmyk.c != 0 || cmyk.m != 0 || cmyk.y != 0) {
                        grayIndexed = false;
                    }
                    colorMap->getDeviceN(&pix, &deviceN);
                    for (int cp = 0; cp < SPOT_NCOMPS + 4; cp++) {
                        imgData.lookup[(SPOT_NCOMPS + 4) * i + cp] = colToByte(deviceN.c[cp]);
                    }
                }
            }
            break;
        }
    }

    setOverprintMask(colorMap->getColorSpace(), state->getFillOverprint(), state->getOverprintMode(), nullptr, grayIndexed);

    if (colorMode == splashModeMono1) {
        srcMode = splashModeMono8;
    } else {
        srcMode = colorMode;
    }
#if USE_CMS
    src = maskColors ? &alphaImageSrc : useIccImageSrc(&imgData) ? &iccImageSrc : &imageSrc;
    tf = maskColors == nullptr && useIccImageSrc(&imgData) ? &iccTransform : nullptr;
#else
    src = maskColors ? &alphaImageSrc : &imageSrc;
    tf = nullptr;
#endif
    splash->drawImage(src, tf, &imgData, srcMode, maskColors != nullptr, width, height, mat, interpolate);
    if (inlineImg) {
        while (imgData.y < height) {
            imgData.imgStr->getLine();
            ++imgData.y;
        }
    }

    gfree(imgData.lookup);
    str->close();
}

struct SplashOutMaskedImageData
{
    std::unique_ptr<ImageStream> imgStr;
    GfxImageColorMap *colorMap;
    std::unique_ptr<SplashBitmap> mask;
    SplashColorPtr lookup;
    SplashColorMode colorMode;
    int width, height, y;
};

bool SplashOutputDev::maskedImageSrc(void *data, SplashColorPtr colorLine, unsigned char *alphaLine)
{
    auto *imgData = static_cast<SplashOutMaskedImageData *>(data);
    unsigned char *p, *aq;
    SplashColorPtr q, col;
    GfxRGB rgb;
    GfxGray gray;
    GfxCMYK cmyk;
    GfxColor deviceN;
    unsigned char alpha;
    const unsigned char *maskPtr;
    int maskBit;
    int nComps, x;

    if (imgData->y == imgData->height) {
        return false;
    }
    if (!(p = imgData->imgStr->getLine())) {
        return false;
    }

    nComps = imgData->colorMap->getNumPixelComps();

    maskPtr = imgData->mask->getDataPtr() + imgData->y * imgData->mask->getRowSize();
    maskBit = 0x80;
    for (x = 0, q = colorLine, aq = alphaLine; x < imgData->width; ++x, p += nComps) {
        alpha = (*maskPtr & maskBit) ? 0xff : 0x00;
        if (!(maskBit >>= 1)) {
            ++maskPtr;
            maskBit = 0x80;
        }
        if (imgData->lookup) {
            switch (imgData->colorMode) {
            case splashModeMono1:
            case splashModeMono8:
                *q++ = imgData->lookup[*p];
                break;
            case splashModeRGB8:
            case splashModeBGR8:
                col = &imgData->lookup[3 * *p];
                *q++ = col[0];
                *q++ = col[1];
                *q++ = col[2];
                break;
            case splashModeXBGR8:
                col = &imgData->lookup[4 * *p];
                *q++ = col[0];
                *q++ = col[1];
                *q++ = col[2];
                *q++ = 255;
                break;
            case splashModeCMYK8:
                col = &imgData->lookup[4 * *p];
                *q++ = col[0];
                *q++ = col[1];
                *q++ = col[2];
                *q++ = col[3];
                break;
            case splashModeDeviceN8:
                col = &imgData->lookup[(SPOT_NCOMPS + 4) * *p];
                for (int cp = 0; cp < SPOT_NCOMPS + 4; cp++) {
                    *q++ = col[cp];
                }
                break;
            }
            *aq++ = alpha;
        } else {
            switch (imgData->colorMode) {
            case splashModeMono1:
            case splashModeMono8:
                imgData->colorMap->getGray(p, &gray);
                *q++ = colToByte(gray);
                break;
            case splashModeXBGR8:
            case splashModeRGB8:
            case splashModeBGR8:
                imgData->colorMap->getRGB(p, &rgb);
                *q++ = colToByte(rgb.r);
                *q++ = colToByte(rgb.g);
                *q++ = colToByte(rgb.b);
                if (imgData->colorMode == splashModeXBGR8) {
                    *q++ = 255;
                }
                break;
            case splashModeCMYK8:
                imgData->colorMap->getCMYK(p, &cmyk);
                *q++ = colToByte(cmyk.c);
                *q++ = colToByte(cmyk.m);
                *q++ = colToByte(cmyk.y);
                *q++ = colToByte(cmyk.k);
                break;
            case splashModeDeviceN8:
                imgData->colorMap->getDeviceN(p, &deviceN);
                for (int cp = 0; cp < SPOT_NCOMPS + 4; cp++) {
                    *q++ = colToByte(deviceN.c[cp]);
                }
                break;
            }
            *aq++ = alpha;
        }
    }

    ++imgData->y;
    return true;
}

void SplashOutputDev::drawMaskedImage(GfxState *state, Object *ref, Stream *str, int width, int height, GfxImageColorMap *colorMap, bool interpolate, Stream *maskStr, int maskWidth, int maskHeight, bool maskInvert, bool maskInterpolate)
{
    std::array<double, 6> mat;
    SplashOutMaskedImageData imgData;
    SplashOutImageMaskData imgMaskData;
    SplashColorMode srcMode;
    SplashColor maskColor;
    GfxGray gray;
    GfxRGB rgb;
    GfxCMYK cmyk;
    GfxColor deviceN;
    unsigned char pix;
    int n, i;

    colorMap->getColorSpace()->createMapping(bitmap->getSeparationList(), SPOT_NCOMPS);
    setOverprintMask(colorMap->getColorSpace(), state->getFillOverprint(), state->getOverprintMode(), nullptr);

    // If the mask is higher resolution than the image, use
    // drawSoftMaskedImage() instead.
    if (maskWidth > width || maskHeight > height) {
        auto maskDecodeArray = std::make_unique<Array>((xref) ? xref : doc->getXRef());
        maskDecodeArray->add(Object(maskInvert ? 0 : 1));
        maskDecodeArray->add(Object(maskInvert ? 1 : 0));
        Object maskDecode(std::move(maskDecodeArray));
        GfxImageColorMap maskColorMap(1, &maskDecode, std::make_unique<GfxDeviceGrayColorSpace>());
        drawSoftMaskedImage(state, ref, str, width, height, colorMap, interpolate, maskStr, maskWidth, maskHeight, &maskColorMap, maskInterpolate);

    } else {
        //----- scale the mask image to the same size as the source image

        mat[0] = static_cast<double>(width);
        mat[1] = 0;
        mat[2] = 0;
        mat[3] = static_cast<double>(height);
        mat[4] = 0;
        mat[5] = 0;
        imgMaskData.imgStr = std::make_unique<ImageStream>(maskStr, maskWidth, 1, 1);
        if (!imgMaskData.imgStr->rewind()) {
            return;
        }
        imgMaskData.invert = !maskInvert;
        imgMaskData.width = maskWidth;
        imgMaskData.height = maskHeight;
        imgMaskData.y = 0;
        auto maskBitmap = std::make_unique<SplashBitmap>(width, height, 1, splashModeMono1, false);
        if (!maskBitmap->getDataPtr()) {
            width = height = 1;
            maskBitmap = std::make_unique<SplashBitmap>(width, height, 1, splashModeMono1, false);
        }
        {
            Splash maskSplash { maskBitmap.get(), false };
            maskColor[0] = 0;
            maskSplash.clear(maskColor);
            maskColor[0] = 0xff;
            maskSplash.setFillPattern(new SplashSolidColor(maskColor));
            maskSplash.fillImageMask(&imageMaskSrc, &imgMaskData, maskWidth, maskHeight, mat, false);
            maskStr->close();
        }

        //----- draw the source image

        const std::array<double, 6> &ctm = state->getCTM();
        for (i = 0; i < 6; ++i) {
            if (!std::isfinite(ctm[i])) {
                return;
            }
        }
        mat[0] = ctm[0];
        mat[1] = ctm[1];
        mat[2] = -ctm[2];
        mat[3] = -ctm[3];
        mat[4] = ctm[2] + ctm[4];
        mat[5] = ctm[3] + ctm[5];

        imgData.imgStr = std::make_unique<ImageStream>(str, width, colorMap->getNumPixelComps(), colorMap->getBits());
        if (!imgData.imgStr->rewind()) {
            return;
        }
        imgData.colorMap = colorMap;
        imgData.mask = std::move(maskBitmap);
        imgData.colorMode = colorMode;
        imgData.width = width;
        imgData.height = height;
        imgData.y = 0;

        // special case for one-channel (monochrome/gray/separation) images:
        // build a lookup table here
        imgData.lookup = nullptr;
        if (colorMap->getNumPixelComps() == 1) {
            n = 1 << colorMap->getBits();
            switch (colorMode) {
            case splashModeMono1:
            case splashModeMono8:
                imgData.lookup = static_cast<SplashColorPtr>(gmalloc(n));
                for (i = 0; i < n; ++i) {
                    pix = static_cast<unsigned char>(i);
                    colorMap->getGray(&pix, &gray);
                    imgData.lookup[i] = colToByte(gray);
                }
                break;
            case splashModeRGB8:
            case splashModeBGR8:
                imgData.lookup = static_cast<SplashColorPtr>(gmallocn(n, 3));
                for (i = 0; i < n; ++i) {
                    pix = static_cast<unsigned char>(i);
                    colorMap->getRGB(&pix, &rgb);
                    imgData.lookup[3 * i] = colToByte(rgb.r);
                    imgData.lookup[3 * i + 1] = colToByte(rgb.g);
                    imgData.lookup[3 * i + 2] = colToByte(rgb.b);
                }
                break;
            case splashModeXBGR8:
                imgData.lookup = static_cast<SplashColorPtr>(gmallocn(n, 4));
                for (i = 0; i < n; ++i) {
                    pix = static_cast<unsigned char>(i);
                    colorMap->getRGB(&pix, &rgb);
                    imgData.lookup[4 * i] = colToByte(rgb.r);
                    imgData.lookup[4 * i + 1] = colToByte(rgb.g);
                    imgData.lookup[4 * i + 2] = colToByte(rgb.b);
                    imgData.lookup[4 * i + 3] = 255;
                }
                break;
            case splashModeCMYK8:
                imgData.lookup = static_cast<SplashColorPtr>(gmallocn(n, 4));
                for (i = 0; i < n; ++i) {
                    pix = static_cast<unsigned char>(i);
                    colorMap->getCMYK(&pix, &cmyk);
                    imgData.lookup[4 * i] = colToByte(cmyk.c);
                    imgData.lookup[4 * i + 1] = colToByte(cmyk.m);
                    imgData.lookup[4 * i + 2] = colToByte(cmyk.y);
                    imgData.lookup[4 * i + 3] = colToByte(cmyk.k);
                }
                break;
            case splashModeDeviceN8:
                imgData.lookup = static_cast<SplashColorPtr>(gmallocn(n, SPOT_NCOMPS + 4));
                for (i = 0; i < n; ++i) {
                    pix = static_cast<unsigned char>(i);
                    colorMap->getDeviceN(&pix, &deviceN);
                    for (int cp = 0; cp < SPOT_NCOMPS + 4; cp++) {
                        imgData.lookup[(SPOT_NCOMPS + 4) * i + cp] = colToByte(deviceN.c[cp]);
                    }
                }
                break;
            }
        }

        if (colorMode == splashModeMono1) {
            srcMode = splashModeMono8;
        } else {
            srcMode = colorMode;
        }
        splash->drawImage(&maskedImageSrc, nullptr, &imgData, srcMode, true, width, height, mat, interpolate);
        gfree(imgData.lookup);
        str->close();
    }
}

void SplashOutputDev::drawSoftMaskedImage(GfxState *state, Object * /* ref */, Stream *str, int width, int height, GfxImageColorMap *colorMap, bool interpolate, Stream *maskStr, int maskWidth, int maskHeight, GfxImageColorMap *maskColorMap,
                                          bool maskInterpolate)
{
    std::array<double, 6> mat;
    SplashOutImageData imgData;
    SplashOutImageData imgMaskData;
    SplashColorMode srcMode;
    SplashBitmap *maskBitmap;
    SplashColor maskColor;
    GfxGray gray;
    GfxRGB rgb;
    GfxCMYK cmyk;
    GfxColor deviceN;
    unsigned char pix;
    std::unique_ptr<AutoFreeMemStream> ownedMaskStr;

    colorMap->getColorSpace()->createMapping(bitmap->getSeparationList(), SPOT_NCOMPS);
    setOverprintMask(colorMap->getColorSpace(), state->getFillOverprint(), state->getOverprintMode(), nullptr);

    const std::array<double, 6> &ctm = state->getCTM();
    for (int i = 0; i < 6; ++i) {
        if (!std::isfinite(ctm[i])) {
            return;
        }
    }
    mat[0] = ctm[0];
    mat[1] = ctm[1];
    mat[2] = -ctm[2];
    mat[3] = -ctm[3];
    mat[4] = ctm[2] + ctm[4];
    mat[5] = ctm[3] + ctm[5];

    //----- set up the soft mask

    if (maskColorMap->getMatteColor() != nullptr) {
        int maskChars;
        if (checkedMultiply(maskWidth, maskHeight, &maskChars)) {
            return;
        }
        if (!maskStr->rewind()) {
            return;
        }
        std::vector<char> data;
        data.resize(maskChars);
        const int readChars = maskStr->doGetChars(maskChars, reinterpret_cast<unsigned char *>(data.data()));
        if (unlikely(readChars < maskChars)) {
            memset(&data[readChars], 0, maskChars - readChars);
        }
        maskStr->close();

        ownedMaskStr = std::make_unique<AutoFreeMemStream>(std::move(data), maskStr->getDictObject()->copy());
        maskStr = ownedMaskStr.get();
    }
    imgMaskData.imgStr = std::make_unique<ImageStream>(maskStr, maskWidth, maskColorMap->getNumPixelComps(), maskColorMap->getBits());
    if (!imgMaskData.imgStr->rewind()) {
        return;
    }
    imgMaskData.colorMap = maskColorMap;
    imgMaskData.maskColors = nullptr;
    imgMaskData.colorMode = splashModeMono8;
    imgMaskData.width = maskWidth;
    imgMaskData.height = maskHeight;
    imgMaskData.y = 0;
    imgMaskData.maskColorMap = nullptr;
    const unsigned imgMaskDataLookupSize = 1 << maskColorMap->getBits();
    imgMaskData.lookup = static_cast<SplashColorPtr>(gmalloc(imgMaskDataLookupSize));
    for (unsigned i = 0; i < imgMaskDataLookupSize; ++i) {
        pix = static_cast<unsigned char>(i);
        maskColorMap->getGray(&pix, &gray);
        imgMaskData.lookup[i] = colToByte(gray);
    }
    maskBitmap = new SplashBitmap(bitmap->getWidth(), bitmap->getHeight(), 1, splashModeMono8, false);
    {
        Splash maskSplash { maskBitmap, vectorAntialias };
        maskColor[0] = 0;
        maskSplash.clear(maskColor);
        maskSplash.drawImage(&imageSrc, nullptr, &imgMaskData, splashModeMono8, false, maskWidth, maskHeight, mat, maskInterpolate);
        if (maskColorMap->getMatteColor() == nullptr) {
            maskStr->close();
        }
        gfree(imgMaskData.lookup);
    }
    splash->setSoftMask(maskBitmap);

    //----- draw the source image

    imgData.imgStr = std::make_unique<ImageStream>(str, width, colorMap->getNumPixelComps(), colorMap->getBits());
    if (!imgData.imgStr->rewind()) {
        return;
    }
    imgData.colorMap = colorMap;
    imgData.maskColors = nullptr;
    imgData.colorMode = colorMode;
    imgData.width = width;
    imgData.height = height;
    imgData.maskStr = nullptr;
    imgData.maskColorMap = nullptr;
    if (maskColorMap->getMatteColor() != nullptr) {
        getMatteColor(colorMode, colorMap, *maskColorMap->getMatteColor(), imgData.matteColor);
        imgData.maskColorMap = maskColorMap;
        imgData.maskStr = std::make_unique<ImageStream>(maskStr, maskWidth, maskColorMap->getNumPixelComps(), maskColorMap->getBits());
        if (!imgData.maskStr->rewind()) {
            return;
        }
    }
    imgData.y = 0;

    // special case for one-channel (monochrome/gray/separation) images:
    // build a lookup table here
    imgData.lookup = nullptr;
    if (colorMap->getNumPixelComps() == 1) {
        const unsigned n = 1 << colorMap->getBits();
        switch (colorMode) {
        case splashModeMono1:
        case splashModeMono8:
            imgData.lookup = static_cast<SplashColorPtr>(gmalloc(n));
            for (unsigned i = 0; i < n; ++i) {
                pix = static_cast<unsigned char>(i);
                colorMap->getGray(&pix, &gray);
                imgData.lookup[i] = colToByte(gray);
            }
            break;
        case splashModeRGB8:
        case splashModeBGR8:
            imgData.lookup = static_cast<SplashColorPtr>(gmallocn_checkoverflow(n, 3));
            if (likely(imgData.lookup != nullptr)) {
                for (unsigned i = 0; i < n; ++i) {
                    pix = static_cast<unsigned char>(i);
                    colorMap->getRGB(&pix, &rgb);
                    imgData.lookup[3 * i] = colToByte(rgb.r);
                    imgData.lookup[3 * i + 1] = colToByte(rgb.g);
                    imgData.lookup[3 * i + 2] = colToByte(rgb.b);
                }
            }
            break;
        case splashModeXBGR8:
            imgData.lookup = static_cast<SplashColorPtr>(gmallocn_checkoverflow(n, 4));
            if (likely(imgData.lookup != nullptr)) {
                for (unsigned i = 0; i < n; ++i) {
                    pix = static_cast<unsigned char>(i);
                    colorMap->getRGB(&pix, &rgb);
                    imgData.lookup[4 * i] = colToByte(rgb.r);
                    imgData.lookup[4 * i + 1] = colToByte(rgb.g);
                    imgData.lookup[4 * i + 2] = colToByte(rgb.b);
                    imgData.lookup[4 * i + 3] = 255;
                }
            }
            break;
        case splashModeCMYK8:
            imgData.lookup = static_cast<SplashColorPtr>(gmallocn_checkoverflow(n, 4));
            if (likely(imgData.lookup != nullptr)) {
                for (unsigned i = 0; i < n; ++i) {
                    pix = static_cast<unsigned char>(i);
                    colorMap->getCMYK(&pix, &cmyk);
                    imgData.lookup[4 * i] = colToByte(cmyk.c);
                    imgData.lookup[4 * i + 1] = colToByte(cmyk.m);
                    imgData.lookup[4 * i + 2] = colToByte(cmyk.y);
                    imgData.lookup[4 * i + 3] = colToByte(cmyk.k);
                }
            }
            break;
        case splashModeDeviceN8:
            imgData.lookup = static_cast<SplashColorPtr>(gmallocn_checkoverflow(n, SPOT_NCOMPS + 4));
            if (likely(imgData.lookup != nullptr)) {
                for (unsigned i = 0; i < n; ++i) {
                    pix = static_cast<unsigned char>(i);
                    colorMap->getDeviceN(&pix, &deviceN);
                    for (int cp = 0; cp < SPOT_NCOMPS + 4; cp++) {
                        imgData.lookup[(SPOT_NCOMPS + 4) * i + cp] = colToByte(deviceN.c[cp]);
                    }
                }
            }
            break;
        }
    }

    if (colorMode == splashModeMono1) {
        srcMode = splashModeMono8;
    } else {
        srcMode = colorMode;
    }
    splash->drawImage(&imageSrc, nullptr, &imgData, srcMode, false, width, height, mat, interpolate);
    splash->setSoftMask(nullptr);
    gfree(imgData.lookup);
    if (ownedMaskStr) {
        ownedMaskStr->close();
        ownedMaskStr.reset();
    }
    str->close();
}

bool SplashOutputDev::checkTransparencyGroup(GfxState *state, bool knockout)
{
    if (state->getFillOpacity() != 1 || state->getStrokeOpacity() != 1 || state->getAlphaIsShape() || state->getBlendMode() != gfxBlendNormal || splash->getSoftMask() != nullptr || knockout) {
        return true;
    }
    return transpGroupStack != nullptr && transpGroupStack->shape != nullptr;
}

void SplashOutputDev::beginTransparencyGroup(GfxState *state, const std::array<double, 4> &bbox, GfxColorSpace *blendingColorSpace, bool isolated, bool knockout, bool forSoftMask)
{
    SplashTransparencyGroup *transpGroup;
    double xMin, yMin, xMax, yMax, x, y;
    int tx, ty, w, h;

    // transform the bbox
    state->transform(bbox[0], bbox[1], &x, &y);
    xMin = xMax = x;
    yMin = yMax = y;
    state->transform(bbox[0], bbox[3], &x, &y);
    if (x < xMin) {
        xMin = x;
    } else if (x > xMax) {
        xMax = x;
    }
    if (y < yMin) {
        yMin = y;
    } else if (y > yMax) {
        yMax = y;
    }
    state->transform(bbox[2], bbox[1], &x, &y);
    if (x < xMin) {
        xMin = x;
    } else if (x > xMax) {
        xMax = x;
    }
    if (y < yMin) {
        yMin = y;
    } else if (y > yMax) {
        yMax = y;
    }
    state->transform(bbox[2], bbox[3], &x, &y);
    if (x < xMin) {
        xMin = x;
    } else if (x > xMax) {
        xMax = x;
    }
    if (y < yMin) {
        yMin = y;
    } else if (y > yMax) {
        yMax = y;
    }
    tx = static_cast<int>(floor(xMin));
    if (tx < 0) {
        tx = 0;
    } else if (tx >= bitmap->getWidth()) {
        tx = bitmap->getWidth() - 1;
    }
    ty = static_cast<int>(floor(yMin));
    if (ty < 0) {
        ty = 0;
    } else if (ty >= bitmap->getHeight()) {
        ty = bitmap->getHeight() - 1;
    }
    if (checkedSubtraction(static_cast<int>(ceil(xMax)), tx - 1, &w)) {
        w = 1;
    }
    if (tx + w > bitmap->getWidth()) {
        w = bitmap->getWidth() - tx;
    }
    if (w < 1) {
        w = 1;
    }
    if (checkedSubtraction(static_cast<int>(ceil(yMax)), ty - 1, &h)) {
        h = 1;
    }
    if (ty + h > bitmap->getHeight()) {
        h = bitmap->getHeight() - ty;
    }
    if (h < 1) {
        h = 1;
    }

    // push a new stack entry
    transpGroup = new SplashTransparencyGroup();
    transpGroup->softmask = nullptr;
    transpGroup->tx = tx;
    transpGroup->ty = ty;
    transpGroup->blendingColorSpace = blendingColorSpace;
    transpGroup->isolated = isolated;
    transpGroup->shape = (knockout && !isolated) ? SplashBitmap::copy(bitmap) : nullptr;
    transpGroup->knockout = (knockout && isolated);
    transpGroup->knockoutOpacity = 1.0;
    transpGroup->backdropBitmap = nullptr;
    transpGroup->next = transpGroupStack;
    transpGroupStack = transpGroup;

    // save state
    transpGroup->origBitmap = bitmap;
    transpGroup->origSplash = splash;
    transpGroup->fontAA = fontEngine->getAA();

    //~ this handles the blendingColorSpace arg for soft masks, but
    //~   not yet for transparency groups

    // switch to the blending color space
    if (forSoftMask && isolated && blendingColorSpace) {
        if (blendingColorSpace->getMode() == csDeviceGray || blendingColorSpace->getMode() == csCalGray || (blendingColorSpace->getMode() == csICCBased && blendingColorSpace->getNComps() == 1)) {
            colorMode = splashModeMono8;
        } else if (blendingColorSpace->getMode() == csDeviceRGB || blendingColorSpace->getMode() == csCalRGB || (blendingColorSpace->getMode() == csICCBased && blendingColorSpace->getNComps() == 3)) {
            //~ does this need to use BGR8?
            colorMode = splashModeRGB8;
        } else if (blendingColorSpace->getMode() == csDeviceCMYK || (blendingColorSpace->getMode() == csICCBased && blendingColorSpace->getNComps() == 4)) {
            colorMode = splashModeCMYK8;
        }
    }

    // create the temporary bitmap
    bitmap = new SplashBitmap(w, h, bitmapRowPad, colorMode, true, bitmapTopDown, bitmap->getSeparationList());
    if (!bitmap->getDataPtr()) {
        delete bitmap;
        w = h = 1;
        bitmap = new SplashBitmap(w, h, bitmapRowPad, colorMode, true, bitmapTopDown);
    }
    splash = new Splash(bitmap, vectorAntialias, transpGroup->origSplash->getScreen());
    splash->setThinLineMode(transpGroup->origSplash->getThinLineMode());
    splash->setMinLineWidth(s_minLineWidth);
    //~ Acrobat apparently copies at least the fill and stroke colors, and
    //~ maybe other state(?) -- but not the clipping path (and not sure
    //~ what else)
    //~ [this is likely the same situation as in type3D1()]
    splash->setFillPattern(transpGroup->origSplash->getFillPattern()->copy());
    splash->setStrokePattern(transpGroup->origSplash->getStrokePattern()->copy());
    if (isolated) {
        SplashColor color;
        splashClearColor(color);
        if (colorMode == splashModeXBGR8) {
            color[3] = 255;
        }
        splash->clear(color, 0);
        if (knockout) {
            splash->setInTransparencyGroup(transpGroup->origBitmap, tx, ty, false, true);
        }
    } else {
        if (splash->blitTransparent(*transpGroup->origBitmap, tx, ty, 0, 0, w, h) != SplashError::NoError) {
            SplashColor color;
            splashClearColor(color);
            splash->clear(color, 0);
        }
        if (!isolated && transpGroup->origBitmap->getAlphaPtr() && transpGroup->origSplash->getInNonIsolatedGroup()) {
            // when drawing a non-isolated group into another non-isolated group,
            // compute a backdrop bitmap with corrected alpha values
            auto *backdropBitmap = new SplashBitmap(w, h, bitmapRowPad, colorMode, true, bitmapTopDown);
            transpGroup->origSplash->blitCorrectedAlpha(backdropBitmap, tx, ty, 0, 0, w, h);
            transpGroup->backdropBitmap = backdropBitmap;
            splash->setInTransparencyGroup(backdropBitmap, 0, 0, true, knockout);
        } else {
            SplashBitmap *shape = knockout ? transpGroup->shape : (transpGroup->next != nullptr && transpGroup->next->shape != nullptr) ? transpGroup->next->shape : transpGroup->origBitmap;
            int shapeTx = knockout ? tx : (transpGroup->next != nullptr && transpGroup->next->shape != nullptr) ? transpGroup->next->tx + tx : tx;
            int shapeTy = knockout ? ty : (transpGroup->next != nullptr && transpGroup->next->shape != nullptr) ? transpGroup->next->ty + ty : ty;
            splash->setInTransparencyGroup(shape, shapeTx, shapeTy, true, knockout);
        }
    }
    transpGroup->tBitmap = bitmap;
    state->shiftCTMAndClip(-tx, -ty);
    updateCTM(state, 0, 0, 0, 0, 0, 0);
}

void SplashOutputDev::endTransparencyGroup(GfxState *state)
{
    // restore state
    delete splash;
    bitmap = transpGroupStack->origBitmap;
    colorMode = bitmap->getMode();
    splash = transpGroupStack->origSplash;
    state->shiftCTMAndClip(transpGroupStack->tx, transpGroupStack->ty);
    updateCTM(state, 0, 0, 0, 0, 0, 0);
}

void SplashOutputDev::paintTransparencyGroup(GfxState * /*state*/, const std::array<double, 4> & /*bbox*/)
{
    SplashBitmap *tBitmap;
    SplashTransparencyGroup *transpGroup;
    bool isolated;
    int tx, ty;

    tx = transpGroupStack->tx;
    ty = transpGroupStack->ty;
    tBitmap = transpGroupStack->tBitmap;
    isolated = transpGroupStack->isolated;

    // paint the transparency group onto the parent bitmap
    // - the clip path was set in the parent's state)
    if (tx < bitmap->getWidth() && ty < bitmap->getHeight()) {
        double knockoutOpacity = (transpGroupStack->next != nullptr) ? transpGroupStack->next->knockoutOpacity : transpGroupStack->knockoutOpacity;
        splash->setOverprintMask(0xffffffff, false);
        splash->composite(*tBitmap, 0, 0, tx, ty, tBitmap->getWidth(), tBitmap->getHeight(), false, !isolated, transpGroupStack->next != nullptr && transpGroupStack->next->knockout, knockoutOpacity);
        fontEngine->setAA(transpGroupStack->fontAA);
        if (transpGroupStack->next != nullptr && transpGroupStack->next->shape != nullptr) {
            transpGroupStack->next->knockout = true;
        }
    }

    // pop the stack
    transpGroup = transpGroupStack;
    transpGroupStack = transpGroup->next;
    if (transpGroupStack != nullptr && transpGroup->knockoutOpacity < transpGroupStack->knockoutOpacity) {
        transpGroupStack->knockoutOpacity = transpGroup->knockoutOpacity;
    }
    delete transpGroup->shape;
    delete transpGroup->backdropBitmap;
    delete transpGroup;

    delete tBitmap;
}

void SplashOutputDev::setSoftMask(GfxState * /*state*/, const std::array<double, 4> & /*bbox*/, bool alpha, Function *transferFunc, const GfxColor &backdropColor)
{
    SplashBitmap *tBitmap;
    Splash *tSplash;
    SplashTransparencyGroup *transpGroup;
    SplashColor color;
    SplashColorPtr p;
    GfxGray gray;
    GfxRGB rgb;
    GfxCMYK cmyk;
    GfxColor deviceN;
    double lum, lum2;
    int tx, ty, x, y;

    tx = transpGroupStack->tx;
    ty = transpGroupStack->ty;
    tBitmap = transpGroupStack->tBitmap;

    // composite with backdrop color
    if (!alpha && tBitmap->getMode() != splashModeMono1) {
        //~ need to correctly handle the case where no blending color
        //~ space is given
        if (transpGroupStack->blendingColorSpace) {
            tSplash = new Splash(tBitmap, vectorAntialias, transpGroupStack->origSplash->getScreen());
            switch (tBitmap->getMode()) {
            case splashModeMono1:
                // transparency is not supported in mono1 mode
                break;
            case splashModeMono8:
                transpGroupStack->blendingColorSpace->getGray(backdropColor, &gray);
                color[0] = colToByte(gray);
                tSplash->compositeBackground(color);
                break;
            case splashModeXBGR8:
                color[3] = 255;
                // fallthrough
            case splashModeRGB8:
            case splashModeBGR8:
                transpGroupStack->blendingColorSpace->getRGB(backdropColor, &rgb);
                color[0] = colToByte(rgb.r);
                color[1] = colToByte(rgb.g);
                color[2] = colToByte(rgb.b);
                tSplash->compositeBackground(color);
                break;
            case splashModeCMYK8:
                transpGroupStack->blendingColorSpace->getCMYK(backdropColor, &cmyk);
                color[0] = colToByte(cmyk.c);
                color[1] = colToByte(cmyk.m);
                color[2] = colToByte(cmyk.y);
                color[3] = colToByte(cmyk.k);
                tSplash->compositeBackground(color);
                break;
            case splashModeDeviceN8:
                transpGroupStack->blendingColorSpace->getDeviceN(backdropColor, &deviceN);
                for (int cp = 0; cp < SPOT_NCOMPS + 4; cp++) {
                    color[cp] = colToByte(deviceN.c[cp]);
                }
                tSplash->compositeBackground(color);
                break;
            }
            delete tSplash;
        }
    }

    auto *softMask = new SplashBitmap(bitmap->getWidth(), bitmap->getHeight(), 1, splashModeMono8, false);
    if (!softMask->getDataPtr()) {
        delete softMask;
        softMask = new SplashBitmap(1, 1, 1, splashModeMono8, false);
    }
    unsigned char fill = 0;
    if (transpGroupStack->blendingColorSpace) {
        transpGroupStack->blendingColorSpace->getGray(backdropColor, &gray);
        fill = colToByte(gray);
    }
    memset(softMask->getDataPtr(), fill, softMask->getRowSize() * softMask->getHeight());
    p = softMask->getDataPtr() + ty * softMask->getRowSize() + tx;
    int xMax = tBitmap->getWidth();
    int yMax = tBitmap->getHeight();
    if (xMax > softMask->getWidth() - tx) {
        xMax = softMask->getWidth() - tx;
    }
    if (yMax > softMask->getHeight() - ty) {
        yMax = softMask->getHeight() - ty;
    }
    for (y = 0; y < yMax; ++y) {
        for (x = 0; x < xMax; ++x) {
            if (alpha) {
                if (transferFunc) {
                    lum = tBitmap->getAlpha(x, y) / 255.0;
                    transferFunc->transform(&lum, &lum2);
                    p[x] = static_cast<int>(lum2 * 255.0 + 0.5);
                } else {
                    p[x] = tBitmap->getAlpha(x, y);
                }
            } else {
                tBitmap->getPixel(x, y, color);
                // convert to luminosity
                switch (tBitmap->getMode()) {
                case splashModeMono1:
                case splashModeMono8:
                    lum = color[0] / 255.0;
                    break;
                case splashModeXBGR8:
                case splashModeRGB8:
                case splashModeBGR8:
                    lum = (0.3 / 255.0) * color[0] + (0.59 / 255.0) * color[1] + (0.11 / 255.0) * color[2];
                    break;
                case splashModeCMYK8:
                case splashModeDeviceN8:
                    lum = (1 - color[3] / 255.0) - (0.3 / 255.0) * color[0] - (0.59 / 255.0) * color[1] - (0.11 / 255.0) * color[2];
                    if (lum < 0) {
                        lum = 0;
                    }
                    break;
                }
                if (transferFunc) {
                    transferFunc->transform(&lum, &lum2);
                } else {
                    lum2 = lum;
                }
                p[x] = static_cast<int>(lum2 * 255.0 + 0.5);
            }
        }
        p += softMask->getRowSize();
    }
    splash->setSoftMask(softMask);

    // pop the stack
    transpGroup = transpGroupStack;
    transpGroupStack = transpGroup->next;
    delete transpGroup->backdropBitmap;
    delete transpGroup;

    delete tBitmap;
}

void SplashOutputDev::clearSoftMask(GfxState * /*state*/)
{
    splash->setSoftMask(nullptr);
}

void SplashOutputDev::setPaperColor(SplashColorPtr paperColorA)
{
    splashColorCopy(paperColor, paperColorA);
}

int SplashOutputDev::getBitmapWidth()
{
    return bitmap->getWidth();
}

int SplashOutputDev::getBitmapHeight()
{
    return bitmap->getHeight();
}

SplashBitmap *SplashOutputDev::takeBitmap()
{
    SplashBitmap *ret;

    ret = bitmap;
    bitmap = new SplashBitmap(1, 1, bitmapRowPad, colorMode, colorMode != splashModeMono1, bitmapTopDown);
    return ret;
}

#if 1 //~tmp: turn off anti-aliasing temporarily
bool SplashOutputDev::getVectorAntialias()
{
    return splash->getVectorAntialias();
}

void SplashOutputDev::setVectorAntialias(bool vaa)
{
    vaa = vaa && colorMode != splashModeMono1;
    vectorAntialias = vaa;
    splash->setVectorAntialias(vaa);
}
#endif

void SplashOutputDev::setFreeTypeHinting(bool enable, bool enableSlightHintingA)
{
    enableFreeTypeHinting = enable;
    enableSlightHinting = enableSlightHintingA;
}

bool SplashOutputDev::tilingPatternFill(GfxState *state, Gfx *gfxA, Catalog * /*catalog*/, GfxTilingPattern *tPat, const std::array<double, 6> &mat, int x0, int y0, int x1, int y1, double xStep, double yStep)
{
    PDFRectangle box;
    Splash *formerSplash = splash;
    SplashBitmap *formerBitmap = bitmap;
    double width, height;
    int surface_width, surface_height, result_width, result_height, i;
    int repeatX, repeatY;
    std::array<double, 6> matc;
    Matrix m1;
    double savedCTM[6];
    double kx, ky, sx, sy;
    bool retValue = false;
    const std::array<double, 4> &bbox = tPat->getBBox();
    const std::array<double, 6> &ptm = tPat->getMatrix();
    const int paintType = tPat->getPaintType();
    Dict *resDict = tPat->getResDict();

    width = bbox[2] - bbox[0];
    height = bbox[3] - bbox[1];

    if (xStep != width || yStep != height) {
        return false;
    }

    // calculate offsets
    const std::array<double, 6> &ctm = state->getCTM();
    for (i = 0; i < 6; ++i) {
        savedCTM[i] = ctm[i];
    }
    state->concatCTM(mat[0], mat[1], mat[2], mat[3], mat[4], mat[5]);
    state->concatCTM(1, 0, 0, 1, bbox[0], bbox[1]);
    // ctm now is different because of the concatCTM calls
    for (i = 0; i < 6; ++i) {
        if (!std::isfinite(ctm[i])) {
            state->setCTM(savedCTM[0], savedCTM[1], savedCTM[2], savedCTM[3], savedCTM[4], savedCTM[5]);
            return false;
        }
    }
    matc[4] = x0 * xStep * ctm[0] + y0 * yStep * ctm[2] + ctm[4];
    matc[5] = x0 * xStep * ctm[1] + y0 * yStep * ctm[3] + ctm[5];
    if (splashAbs(ctm[1]) > splashAbs(ctm[0])) {
        kx = -ctm[1];
        ky = ctm[2] - (ctm[0] * ctm[3]) / ctm[1];
    } else {
        kx = ctm[0];
        ky = ctm[3] - (ctm[1] * ctm[2]) / ctm[0];
    }
    result_width = static_cast<int>(ceil(fabs(kx * width * (x1 - x0))));
    result_height = static_cast<int>(ceil(fabs(ky * height * (y1 - y0))));
    kx = state->getHDPI() / 72.0;
    ky = state->getVDPI() / 72.0;
    m1.m[0] = std::max(fabs(ptm[0]), fabs(ptm[2])) * kx;
    m1.m[1] = 0;
    m1.m[2] = 0;
    m1.m[3] = std::max(fabs(ptm[1]), fabs(ptm[3])) * ky;
    m1.m[4] = 0;
    m1.m[5] = 0;
    m1.transform(width, height, &kx, &ky);
    surface_width = static_cast<int>(ceil(fabs(kx)));
    surface_height = static_cast<int>(ceil(fabs(ky)));

    sx = result_width / (surface_width * static_cast<double>(x1 - x0));
    sy = result_height / (surface_height * static_cast<double>(y1 - y0));
    m1.m[0] *= sx;
    m1.m[3] *= sy;
    m1.transform(width, height, &kx, &ky);

    if (fabs(kx) < 1 && fabs(ky) < 1) {
        kx = std::min<double>(kx, ky);
        ky = 2 / kx;
        m1.m[0] *= ky;
        m1.m[3] *= ky;
        m1.transform(width, height, &kx, &ky);
        surface_width = static_cast<int>(ceil(fabs(kx)));
        surface_height = static_cast<int>(ceil(fabs(ky)));
        repeatX = x1 - x0;
        repeatY = y1 - y0;
        while (static_cast<unsigned long>(repeatX) * repeatY > 0x800000L) {
            // try to avoid bogus memory allocation size
            if (repeatX > 1) {
                repeatX /= 2;
            }
            if (repeatY > 1) {
                repeatY /= 2;
            }
        }
    } else {
        if (static_cast<unsigned long>(surface_width) * surface_height > 0x800000L) {
            state->setCTM(savedCTM[0], savedCTM[1], savedCTM[2], savedCTM[3], savedCTM[4], savedCTM[5]);
            return false;
        }
        while (fabs(kx) > 16384 || fabs(ky) > 16384) {
            // limit pattern bitmap size
            m1.m[0] /= 2;
            m1.m[3] /= 2;
            m1.transform(width, height, &kx, &ky);
        }
        surface_width = static_cast<int>(ceil(fabs(kx)));
        surface_height = static_cast<int>(ceil(fabs(ky)));
        // adjust repeat values to completely fill region
        if (unlikely(surface_width == 0 || surface_height == 0)) {
            state->setCTM(savedCTM[0], savedCTM[1], savedCTM[2], savedCTM[3], savedCTM[4], savedCTM[5]);
            return false;
        }
        repeatX = result_width / surface_width;
        repeatY = result_height / surface_height;
        if (surface_width * repeatX < result_width) {
            repeatX++;
        }
        if (surface_height * repeatY < result_height) {
            repeatY++;
        }
        if (x1 - x0 > repeatX) {
            repeatX = x1 - x0;
        }
        if (y1 - y0 > repeatY) {
            repeatY = y1 - y0;
        }
    }
    // restore CTM and calculate rotate and scale with rounded matrix
    state->setCTM(savedCTM[0], savedCTM[1], savedCTM[2], savedCTM[3], savedCTM[4], savedCTM[5]);
    state->concatCTM(mat[0], mat[1], mat[2], mat[3], mat[4], mat[5]);
    state->concatCTM(width * repeatX, 0, 0, height * repeatY, bbox[0], bbox[1]);
    // ctm now is different because of the concatCTM calls
    matc[0] = ctm[0];
    matc[1] = ctm[1];
    matc[2] = ctm[2];
    matc[3] = ctm[3];

    if (surface_width == 0 || surface_height == 0 || repeatX * repeatY <= 4 || checkedMultiply(surface_width, repeatX, &result_width) || checkedMultiply(surface_height, repeatY, &result_height)) {
        state->setCTM(savedCTM[0], savedCTM[1], savedCTM[2], savedCTM[3], savedCTM[4], savedCTM[5]);
        return false;
    }
    m1.transform(bbox[0], bbox[1], &kx, &ky);
    m1.m[4] = -kx;
    m1.m[5] = -ky;

    box.x1 = bbox[0];
    box.y1 = bbox[1];
    box.x2 = bbox[2];
    box.y2 = bbox[3];
    std::unique_ptr<Gfx> gfx = std::make_unique<Gfx>(doc, this, resDict, box, nullptr, nullptr, nullptr, gfxA);
    // set pattern transformation matrix
    gfx->getState()->setCTM(m1.m[0], m1.m[1], m1.m[2], m1.m[3], m1.m[4], m1.m[5]);
    if (splashAbs(matc[1]) > splashAbs(matc[0])) {
        kx = -matc[1];
        ky = matc[2] - (matc[0] * matc[3]) / matc[1];
    } else {
        kx = matc[0];
        ky = matc[3] - (matc[1] * matc[2]) / matc[0];
    }
    kx = result_width / (fabs(kx) + 1);
    ky = result_height / (fabs(ky) + 1);
    state->concatCTM(kx, 0, 0, ky, 0, 0);
    // ctm now is different because of the concatCTM call
    matc[0] = ctm[0];
    matc[1] = ctm[1];
    matc[2] = ctm[2];
    matc[3] = ctm[3];

    const bool doFastBlit = matc[0] > 0 && matc[1] == 0 && matc[2] == 0 && matc[3] > 0;
    bitmap = new SplashBitmap(surface_width, surface_height, 1, (paintType == 1 || doFastBlit) ? colorMode : splashModeMono8, true);
    if (bitmap->getDataPtr() == nullptr) {
        SplashBitmap *tBitmap = bitmap;
        bitmap = formerBitmap;
        delete tBitmap;
        state->setCTM(savedCTM[0], savedCTM[1], savedCTM[2], savedCTM[3], savedCTM[4], savedCTM[5]);
        return false;
    }
    splash = new Splash(bitmap, true);
    updateCTM(gfx->getState(), m1.m[0], m1.m[1], m1.m[2], m1.m[3], m1.m[4], m1.m[5]);

    if (paintType == 2) {
        SplashColor clearColor;
        clearColor[0] = (colorMode == splashModeCMYK8 || colorMode == splashModeDeviceN8) ? 0x00 : 0xFF;
        splash->clear(clearColor, 0);
    } else {
        splash->clear(paperColor, 0);
    }
    splash->setThinLineMode(formerSplash->getThinLineMode());
    splash->setMinLineWidth(s_minLineWidth);
    if (doFastBlit) {
        // drawImage would colorize the greyscale pattern in tilingBitmapSrc buffer accessor while tiling.
        // blitImage can't, it has no buffer accessor. We instead colorize the pattern prototype in advance.
        splash->setFillPattern(formerSplash->getFillPattern()->copy());
        splash->setStrokePattern(formerSplash->getStrokePattern()->copy());
    }
    gfx->display(tPat->getContentStream());
    delete splash;
    splash = formerSplash;

    TilingSplashOutBitmap imgData;
    imgData.bitmap = bitmap;
    imgData.paintType = paintType;
    imgData.pattern = splash->getFillPattern();
    imgData.colorMode = colorMode;
    imgData.y = 0;
    imgData.repeatX = repeatX;
    imgData.repeatY = repeatY;
    SplashBitmap *tBitmap = bitmap;
    bitmap = formerBitmap;
    if (doFastBlit) {
        // draw the tiles
        for (int y = 0; y < imgData.repeatY; ++y) {
            for (int x = 0; x < imgData.repeatX; ++x) {
                x0 = splashFloor(matc[4]) + x * tBitmap->getWidth();
                y0 = splashFloor(matc[5]) + y * tBitmap->getHeight();
                splash->blitImage(*tBitmap, true, x0, y0);
            }
        }
        retValue = true;
    } else {
        retValue = splash->drawImage(&tilingBitmapSrc, nullptr, &imgData, colorMode, true, result_width, result_height, matc, false, true) == SplashError::NoError;
    }
    delete tBitmap;
    if (!retValue) {
        state->setCTM(savedCTM[0], savedCTM[1], savedCTM[2], savedCTM[3], savedCTM[4], savedCTM[5]);
    }
    return retValue;
}

bool SplashOutputDev::gouraudTriangleShadedFill(GfxState * /*state*/, GfxGouraudTriangleShading *shading)
{
    GfxColorSpaceMode shadingMode = shading->getColorSpace()->getMode();
    bool bDirectColorTranslation = false; // triggers an optimization.
    switch (colorMode) {
    case splashModeRGB8:
        bDirectColorTranslation = (shadingMode == csDeviceRGB);
        break;
    case splashModeCMYK8:
    case splashModeDeviceN8:
        bDirectColorTranslation = (shadingMode == csDeviceCMYK);
        break;
    default:
        break;
    }
    // restore vector antialias because we support it here
    SplashGouraudPattern splashShading(bDirectColorTranslation, shading);
    const bool vaa = getVectorAntialias();
    setVectorAntialias(true);
    const bool retVal = splash->gouraudTriangleShadedFill(&splashShading);
    setVectorAntialias(vaa);
    return retVal;
}

bool SplashOutputDev::univariateShadedFill(GfxState *state, SplashUnivariatePattern *pattern)
{
    double xMin, yMin, xMax, yMax;
    bool vaa = getVectorAntialias();
    // restore vector antialias because we support it here
    setVectorAntialias(true);

    bool retVal = false;
    // get the clip region bbox
    if (pattern->getShading()->getHasBBox()) {
        pattern->getShading()->getBBox(&xMin, &yMin, &xMax, &yMax);
    } else {
        state->getClipBBox(&xMin, &yMin, &xMax, &yMax);

        xMin = floor(xMin);
        yMin = floor(yMin);
        xMax = ceil(xMax);
        yMax = ceil(yMax);

        {
            Matrix ctm, ictm;
            double x[4], y[4];
            int i;

            state->getCTM(&ctm);
            ctm.invertTo(&ictm);

            ictm.transform(xMin, yMin, &x[0], &y[0]);
            ictm.transform(xMax, yMin, &x[1], &y[1]);
            ictm.transform(xMin, yMax, &x[2], &y[2]);
            ictm.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]);
            }
        }
    }

    // fill the region
    state->moveTo(xMin, yMin);
    state->lineTo(xMax, yMin);
    state->lineTo(xMax, yMax);
    state->lineTo(xMin, yMax);
    state->closePath();
    const SplashPath path = convertPath(state->getPath(), true);

    pattern->getShading()->getColorSpace()->createMapping(bitmap->getSeparationList(), SPOT_NCOMPS);
    setOverprintMask(pattern->getShading()->getColorSpace(), state->getFillOverprint(), state->getOverprintMode(), nullptr);
    // If state->getStrokePattern() is set, then the current clipping region
    // is a stroke path.
    retVal = (splash->shadedFill(path, pattern->getShading()->getHasBBox(), *pattern, (state->getStrokePattern() != nullptr)) == SplashError::NoError);
    state->clearPath();
    setVectorAntialias(vaa);

    return retVal;
}

bool SplashOutputDev::functionShadedFill(GfxState *state, GfxFunctionShading *shading)
{
    SplashFunctionPattern pattern(colorMode, state, shading);
    double xMin, yMin, xMax, yMax;
    bool vaa = getVectorAntialias();
    // restore vector antialias because we support it here
    setVectorAntialias(true);

    bool retVal = false;
    // get the clip region bbox
    if (pattern.getShading()->getHasBBox()) {
        pattern.getShading()->getBBox(&xMin, &yMin, &xMax, &yMax);
    } else {
        state->getClipBBox(&xMin, &yMin, &xMax, &yMax);

        xMin = floor(xMin);
        yMin = floor(yMin);
        xMax = ceil(xMax);
        yMax = ceil(yMax);

        {
            Matrix ctm, ictm;
            double x[4], y[4];
            int i;

            state->getCTM(&ctm);
            ctm.invertTo(&ictm);

            ictm.transform(xMin, yMin, &x[0], &y[0]);
            ictm.transform(xMax, yMin, &x[1], &y[1]);
            ictm.transform(xMin, yMax, &x[2], &y[2]);
            ictm.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]);
            }
        }
    }

    // fill the region
    state->moveTo(xMin, yMin);
    state->lineTo(xMax, yMin);
    state->lineTo(xMax, yMax);
    state->lineTo(xMin, yMax);
    state->closePath();
    const SplashPath path = convertPath(state->getPath(), true);

    pattern.getShading()->getColorSpace()->createMapping(bitmap->getSeparationList(), SPOT_NCOMPS);
    setOverprintMask(pattern.getShading()->getColorSpace(), state->getFillOverprint(), state->getOverprintMode(), nullptr);
    // If state->getStrokePattern() is set, then the current clipping region
    // is a stroke path.
    retVal = (splash->shadedFill(path, pattern.getShading()->getHasBBox(), pattern, (state->getStrokePattern() != nullptr)) == SplashError::NoError);
    state->clearPath();
    setVectorAntialias(vaa);

    return retVal;
}

bool SplashOutputDev::axialShadedFill(GfxState *state, GfxAxialShading *shading, double /*tMin*/, double /*tMax*/)
{
    auto *pattern = new SplashAxialPattern(colorMode, state, shading);
    bool retVal = univariateShadedFill(state, pattern);

    delete pattern;

    return retVal;
}

bool SplashOutputDev::radialShadedFill(GfxState *state, GfxRadialShading *shading, double /*tMin*/, double /*tMax*/)
{
    auto *pattern = new SplashRadialPattern(colorMode, state, shading);
    bool retVal = univariateShadedFill(state, pattern);

    delete pattern;

    return retVal;
}
