/*
    SPDX-FileCopyrightText: 2006, 2009 Brad Hards <bradh@frogmouth.net>

    SPDX-License-Identifier: GPL-2.0-or-later
*/

#include "generator_xps.h"

#include <KAboutData>
#include <KLocalizedString>
#include <QBuffer>
#include <QDateTime>
#include <QFile>
#include <QFontDatabase>
#include <QImageReader>
#include <QList>
#include <QMutex>
#include <QPainter>
#include <QPrinter>
#include <QUrl>

#include <core/area.h>
#include <core/document.h>
#include <core/fileprinter.h>
#include <core/page.h>

K_PLUGIN_CLASS_WITH_JSON(XpsGenerator, "libokularGenerator_xps.json")

Q_DECLARE_METATYPE(QGradient *)
Q_DECLARE_METATYPE(XpsPathFigure *)
Q_DECLARE_METATYPE(XpsPathGeometry *)

// From Qt4
static int hex2int(char hex)
{
    QChar hexchar = QLatin1Char(hex);
    int v;
    if (hexchar.isDigit()) {
        v = hexchar.digitValue();
    } else if (hexchar >= QLatin1Char('A') && hexchar <= QLatin1Char('F')) {
        v = hexchar.cell() - 'A' + 10;
    } else if (hexchar >= QLatin1Char('a') && hexchar <= QLatin1Char('f')) {
        v = hexchar.cell() - 'a' + 10;
    } else {
        v = -1;
    }
    return v;
}

// Modified from Qt4
static QColor hexToRgba(const QByteArray &name)
{
    const int len = name.length();
    if (len == 0 || name[0] != '#') {
        return QColor();
    }
    int r, g, b;
    int a = 255;
    if (len == 7) {
        r = (hex2int(name[1]) << 4) + hex2int(name[2]);
        g = (hex2int(name[3]) << 4) + hex2int(name[4]);
        b = (hex2int(name[5]) << 4) + hex2int(name[6]);
    } else if (len == 9) {
        a = (hex2int(name[1]) << 4) + hex2int(name[2]);
        r = (hex2int(name[3]) << 4) + hex2int(name[4]);
        g = (hex2int(name[5]) << 4) + hex2int(name[6]);
        b = (hex2int(name[7]) << 4) + hex2int(name[8]);
    } else {
        r = g = b = -1;
    }
    if ((uint)r > 255 || (uint)g > 255 || (uint)b > 255) {
        return QColor();
    }
    return QColor(r, g, b, a);
}

static QRectF stringToRectF(QStringView data)
{
    const QList<QStringView> numbers = data.split(QLatin1Char(','));
    QPointF origin(numbers.at(0).toDouble(), numbers.at(1).toDouble());
    QSizeF size(numbers.at(2).toDouble(), numbers.at(3).toDouble());
    return QRectF(origin, size);
}

static bool parseGUID(const QString &guidString, unsigned short guid[16])
{
    if (guidString.length() <= 35) {
        return false;
    }

    // Maps bytes to positions in guidString
    const static int indexes[] = {6, 4, 2, 0, 11, 9, 16, 14, 19, 21, 24, 26, 28, 30, 32, 34};

    for (int i = 0; i < 16; i++) {
        int hex1 = hex2int(guidString[indexes[i]].cell());
        int hex2 = hex2int(guidString[indexes[i] + 1].cell());

        if ((hex1 < 0) || (hex2 < 0)) {
            return false;
        }

        guid[i] = hex1 * 16 + hex2;
    }

    return true;
}

// Read next token of abbreviated path data
static bool nextAbbPathToken(AbbPathToken *token)
{
    int *curPos = &token->curPos;
    QString data = token->data;

    while ((*curPos < data.length()) && (data.at(*curPos).isSpace())) {
        (*curPos)++;
    }

    if (*curPos == data.length()) {
        token->type = abtEOF;
        return true;
    }

    QChar ch = data.at(*curPos);

    if (ch.isNumber() || (ch == QLatin1Char('+')) || (ch == QLatin1Char('-'))) {
        int start = *curPos;
        while ((*curPos < data.length()) && (!data.at(*curPos).isSpace()) && (data.at(*curPos) != QLatin1Char(',') && (!data.at(*curPos).isLetter() || data.at(*curPos) == QLatin1Char('e')))) {
            (*curPos)++;
        }
        token->number = QStringView {data}.mid(start, *curPos - start).toDouble();
        token->type = abtNumber;

    } else if (ch == QLatin1Char(',')) {
        token->type = abtComma;
        (*curPos)++;
    } else if (ch.isLetter()) {
        token->type = abtCommand;
        token->command = data.at(*curPos).cell();
        (*curPos)++;
    } else {
        (*curPos)++;
        return false;
    }

    return true;
}

/**
    Read point (two reals delimited by comma) from abbreviated path data
*/
static QPointF getPointFromString(AbbPathToken *token, bool relative, const QPointF currentPosition)
{
    // TODO Check grammar

    QPointF result;
    result.rx() = token->number;
    nextAbbPathToken(token);
    nextAbbPathToken(token); // ,
    result.ry() = token->number;
    nextAbbPathToken(token);

    if (relative) {
        result += currentPosition;
    }

    return result;
}

/**
    Read point (two reals delimited by comma) from string
*/
static QPointF getPointFromString(QStringView string)
{
    const int commaPos = string.indexOf(QLatin1Char(QLatin1Char(',')));
    if (commaPos == -1 || string.indexOf(QLatin1Char(QLatin1Char(',')), commaPos + 1) != -1) {
        return QPointF();
    }

    QPointF result;
    bool ok = false;
    QStringView ref = string.mid(0, commaPos);
    result.setX(ref.toDouble(&ok));
    if (!ok) {
        return QPointF();
    }

    ref = string.mid(commaPos + 1);
    result.setY(ref.toDouble(&ok));
    if (!ok) {
        return QPointF();
    }

    return result;
}

static Qt::FillRule fillRuleFromString(const QString &data, Qt::FillRule def = Qt::OddEvenFill)
{
    if (data == QLatin1String("EvenOdd")) {
        return Qt::OddEvenFill;
    } else if (data == QLatin1String("NonZero")) {
        return Qt::WindingFill;
    }
    return def;
}

/**
    Parse an abbreviated path "Data" description
    \param data the string containing the whitespace separated values

    \see XPS specification 4.2.3 and Appendix G
*/
static QPainterPath parseAbbreviatedPathData(const QString &data)
{
    QPainterPath path;

    AbbPathToken token;

    token.data = data;
    token.curPos = 0;

    nextAbbPathToken(&token);

    // Used by Smooth cubic curve (command s)
    char lastCommand = ' ';
    QPointF lastSecondControlPoint;

    while (true) {
        if (token.type != abtCommand) {
            if (token.type != abtEOF) {
                qCWarning(OkularXpsDebug).nospace() << "Error in parsing abbreviated path data (" << token.type << "@" << token.curPos << "): " << data;
            }
            return path;
        }

        char command = QChar::fromLatin1(token.command).toLower().cell();
        bool isRelative = QChar::fromLatin1(token.command).isLower();
        QPointF currPos = path.currentPosition();
        nextAbbPathToken(&token);

        switch (command) {
        case 'f':
            int rule;
            rule = (int)token.number;
            if (rule == 0) {
                path.setFillRule(Qt::OddEvenFill);
            } else if (rule == 1) {
                // In xps specs rule 1 means NonZero fill. I think it's equivalent to WindingFill but I'm not sure
                path.setFillRule(Qt::WindingFill);
            }
            nextAbbPathToken(&token);
            break;
        case 'm': // Move
            while (token.type == abtNumber) {
                QPointF point = getPointFromString(&token, isRelative, currPos);
                path.moveTo(point);
            }
            break;
        case 'l': // Line
            while (token.type == abtNumber) {
                QPointF point = getPointFromString(&token, isRelative, currPos);
                path.lineTo(point);
            }
            break;
        case 'h': // Horizontal line
            while (token.type == abtNumber) {
                double x = token.number;
                if (isRelative) {
                    x += path.currentPosition().x();
                }
                path.lineTo(x, path.currentPosition().y());
                nextAbbPathToken(&token);
            }
            break;
        case 'v': // Vertical line
            while (token.type == abtNumber) {
                double y = token.number;
                if (isRelative) {
                    y += path.currentPosition().y();
                }
                path.lineTo(path.currentPosition().x(), y);
                nextAbbPathToken(&token);
            }
            break;
        case 'c': // Cubic bezier curve
            while (token.type == abtNumber) {
                QPointF firstControl = getPointFromString(&token, isRelative, currPos);
                QPointF secondControl = getPointFromString(&token, isRelative, currPos);
                QPointF endPoint = getPointFromString(&token, isRelative, currPos);
                path.cubicTo(firstControl, secondControl, endPoint);

                lastSecondControlPoint = secondControl;
            }
            break;
        case 'q': // Quadratic bezier curve
            while (token.type == abtNumber) {
                QPointF point1 = getPointFromString(&token, isRelative, currPos);
                QPointF point2 = getPointFromString(&token, isRelative, currPos);
                path.quadTo(point1, point2);
            }
            break;
        case 's': // Smooth cubic bezier curve
            while (token.type == abtNumber) {
                QPointF firstControl;
                if ((lastCommand == 's') || (lastCommand == 'c')) {
                    firstControl = lastSecondControlPoint + (lastSecondControlPoint + path.currentPosition());
                } else {
                    firstControl = path.currentPosition();
                }
                QPointF secondControl = getPointFromString(&token, isRelative, currPos);
                QPointF endPoint = getPointFromString(&token, isRelative, currPos);
                path.cubicTo(firstControl, secondControl, endPoint);
            }
            break;
        case 'a': // Arc
            // TODO Implement Arc drawing
            while (token.type == abtNumber) {
                /*QPointF rp =*/getPointFromString(&token, isRelative, currPos);
                /*double r = token.number;*/
                nextAbbPathToken(&token);
                /*double fArc = token.number; */
                nextAbbPathToken(&token);
                /*double fSweep = token.number; */
                nextAbbPathToken(&token);
                /*QPointF point = */ getPointFromString(&token, isRelative, currPos);
            }
            break;
        case 'z': // Close path
            path.closeSubpath();
            break;
        }

        lastCommand = command;
    }

    return path;
}

/**
   Parse a "Matrix" attribute string
   \param csv the comma separated list of values
   \return the QTransform corresponding to the affine transform
   given in the attribute

   \see XPS specification 7.4.1
*/
static QTransform attsToMatrix(const QString &csv)
{
    QStringList values = csv.split(QLatin1Char(','));
    if (values.count() != 6) {
        return QTransform(); // that is an identity matrix - no effect
    }
    return QTransform(values.at(0).toDouble(), values.at(1).toDouble(), values.at(2).toDouble(), values.at(3).toDouble(), values.at(4).toDouble(), values.at(5).toDouble());
}

/**
   \return Brush with given color or brush specified by reference to resource
*/
static QBrush parseRscRefColorForBrush(const QString &data)
{
    if (data[0] == QLatin1Char('{')) {
        // TODO
        qCWarning(OkularXpsDebug) << "Reference" << data;
        return QBrush();
    } else {
        return QBrush(hexToRgba(data.toLatin1()));
    }
}

/**
   \return Pen with given color or Pen specified by reference to resource
*/
static QPen parseRscRefColorForPen(const QString &data)
{
    if (data[0] == QLatin1Char('{')) {
        // TODO
        qCWarning(OkularXpsDebug) << "Reference" << data;
        return QPen();
    } else {
        return QPen(hexToRgba(data.toLatin1()));
    }
}

/**
   \return Matrix specified by given data or by referenced dictionary
*/
static QTransform parseRscRefMatrix(const QString &data)
{
    if (data[0] == QLatin1Char('{')) {
        // TODO
        qCWarning(OkularXpsDebug) << "Reference" << data;
        return QTransform();
    } else {
        return attsToMatrix(data);
    }
}

/**
   \return Path specified by given data or by referenced dictionary
*/
static QPainterPath parseRscRefPath(const QString &data)
{
    if (data[0] == QLatin1Char('{')) {
        // TODO
        qCWarning(OkularXpsDebug) << "Reference" << data;
        return QPainterPath();
    } else {
        return parseAbbreviatedPathData(data);
    }
}

/**
   \return The path of the entry
*/
static QString entryPath(const QString &entry)
{
    const QChar slash = QChar::fromLatin1('/');
    const int index = entry.lastIndexOf(slash);
    QString ret = entry.mid(0, index);
    if (index > 0) {
        ret.append(slash);
    }
    if (!ret.startsWith(slash)) {
        ret.prepend(slash);
    }
    return ret;
}

/**
   \return The path of the entry
*/
static QString entryPath(const KZipFileEntry *entry)
{
    return entryPath(entry->path());
}

/**
   \return The absolute path of the \p location, according to \p path if it's non-absolute
*/
static QString absolutePath(const QString &path, const QString &location)
{
    QString retPath;
    if (location.startsWith(QLatin1Char('/'))) {
        // already absolute
        retPath = location;
    } else {
        QUrl u = QUrl::fromLocalFile(path);
        QUrl u2 = QUrl(location);
        retPath = u.resolved(u2).toDisplayString(QUrl::PreferLocalFile);
    }
    // it seems paths & file names can also be percent-encoded
    // (XPS won't ever finish surprising me)
    if (retPath.contains(QLatin1Char('%'))) {
        retPath = QUrl::fromPercentEncoding(retPath.toUtf8());
    }
    return retPath;
}

/**
   Read the content of an archive entry in both the cases:
   a) single file
      + foobar
   b) directory
      + foobar/
        + [0].piece
        + [1].piece
        + ...
        + [x].last.piece

   \see XPS specification 10.1.2
*/
static QByteArray readFileOrDirectoryParts(const KArchiveEntry *entry, QString *pathOfFile = nullptr)
{
    QByteArray data;
    if (entry->isDirectory()) {
        const KArchiveDirectory *relDir = static_cast<const KArchiveDirectory *>(entry);
        QStringList entries = relDir->entries();
        std::sort(entries.begin(), entries.end());
        for (const QString &entryElem : std::as_const(entries)) {
            const KArchiveEntry *relSubEntry = relDir->entry(entryElem);
            if (!relSubEntry->isFile()) {
                continue;
            }

            const KZipFileEntry *relSubFile = static_cast<const KZipFileEntry *>(relSubEntry);
            data.append(relSubFile->data());
        }
    } else {
        const KZipFileEntry *relFile = static_cast<const KZipFileEntry *>(entry);
        data.append(relFile->data());
        if (pathOfFile) {
            *pathOfFile = entryPath(relFile);
        }
    }
    return data;
}

/**
   Load the resource \p fileName from the specified \p archive using the case sensitivity \p cs
*/
static const KArchiveEntry *loadEntry(KZip *archive, const QString &fileName, Qt::CaseSensitivity cs)
{
    // first attempt: loading the entry straight as requested
    const KArchiveEntry *entry = archive->directory()->entry(fileName);
    // in case sensitive mode, or if we actually found something, return what we found
    if (cs == Qt::CaseSensitive || entry) {
        return entry;
    }

    QString path;
    QString entryName;
    const int index = fileName.lastIndexOf(QChar::fromLatin1('/'));
    if (index > 0) {
        path = fileName.left(index);
        entryName = fileName.mid(index + 1);
    } else {
        path = QLatin1Char('/');
        entryName = fileName;
    }
    const KArchiveEntry *newEntry = archive->directory()->entry(path);
    if (newEntry->isDirectory()) {
        const KArchiveDirectory *relDir = static_cast<const KArchiveDirectory *>(newEntry);
        QStringList relEntries = relDir->entries();
        std::sort(relEntries.begin(), relEntries.end());
        for (const QString &relEntry : std::as_const(relEntries)) {
            if (relEntry.compare(entryName, Qt::CaseInsensitive) == 0) {
                return relDir->entry(relEntry);
            }
        }
    }
    return nullptr;
}

/**
   \return The name of a resource from the \p fileName
*/
static QString resourceName(const QString &fileName)
{
    QString resource = fileName;
    const int slashPos = fileName.lastIndexOf(QLatin1Char('/'));
    const int dotPos = fileName.lastIndexOf(QLatin1Char('.'));
    if (slashPos > -1) {
        if (dotPos > -1 && dotPos > slashPos) {
            resource = fileName.mid(slashPos + 1, dotPos - slashPos - 1);
        } else {
            resource = fileName.mid(slashPos + 1);
        }
    }
    return resource;
}

static QColor interpolatedColor(const QColor &c1, const QColor &c2)
{
    QColor res;
    res.setAlpha((c1.alpha() + c2.alpha()) / 2);
    res.setRed((c1.red() + c2.red()) / 2);
    res.setGreen((c1.green() + c2.green()) / 2);
    res.setBlue((c1.blue() + c2.blue()) / 2);
    return res;
}

static bool xpsGradientLessThan(const XpsGradient &g1, const XpsGradient &g2)
{
    return qFuzzyCompare(g1.offset, g2.offset) ? g1.color.name() < g2.color.name() : g1.offset < g2.offset;
}

static int xpsGradientWithOffset(const QList<XpsGradient> &gradients, double offset)
{
    int i = 0;
    for (const XpsGradient &grad : gradients) {
        if (grad.offset == offset) {
            return i;
        }
        ++i;
    }
    return -1;
}

/**
   Preprocess a list of gradients.

   \see XPS specification 11.3.1.1
*/
static void preprocessXpsGradients(QList<XpsGradient> &gradients)
{
    if (gradients.isEmpty()) {
        return;
    }

    // sort the gradients (case 1.)
    std::stable_sort(gradients.begin(), gradients.end(), xpsGradientLessThan);

    // no gradient with stop 0.0 (case 2.)
    if (xpsGradientWithOffset(gradients, 0.0) == -1) {
        int firstGreaterThanZero = 0;
        while (firstGreaterThanZero < gradients.count() && gradients.at(firstGreaterThanZero).offset < 0.0) {
            ++firstGreaterThanZero;
        }
        // case 2.a: no gradients with stop less than 0.0
        if (firstGreaterThanZero == 0) {
            gradients.prepend(XpsGradient(0.0, gradients.first().color));
        }
        // case 2.b: some gradients with stop more than 0.0
        else if (firstGreaterThanZero != gradients.count()) {
            QColor col1 = gradients.at(firstGreaterThanZero - 1).color;
            QColor col2 = gradients.at(firstGreaterThanZero).color;
            for (int i = 0; i < firstGreaterThanZero; ++i) {
                gradients.removeFirst();
            }
            gradients.prepend(XpsGradient(0.0, interpolatedColor(col1, col2)));
        }
        // case 2.c: no gradients with stop more than 0.0
        else {
            XpsGradient newGrad(0.0, gradients.last().color);
            gradients.clear();
            gradients.append(newGrad);
        }
    }

    if (gradients.isEmpty()) {
        return;
    }

    // no gradient with stop 1.0 (case 3.)
    if (xpsGradientWithOffset(gradients, 1.0) == -1) {
        int firstLessThanOne = gradients.count() - 1;
        while (firstLessThanOne >= 0 && gradients.at(firstLessThanOne).offset > 1.0) {
            --firstLessThanOne;
        }
        // case 2.a: no gradients with stop greater than 1.0
        if (firstLessThanOne == gradients.count() - 1) {
            gradients.append(XpsGradient(1.0, gradients.last().color));
        }
        // case 2.b: some gradients with stop more than 1.0
        else if (firstLessThanOne != -1) {
            QColor col1 = gradients.at(firstLessThanOne).color;
            QColor col2 = gradients.at(firstLessThanOne + 1).color;
            for (int i = firstLessThanOne + 1; i < gradients.count(); ++i) {
                gradients.removeLast();
            }
            gradients.append(XpsGradient(1.0, interpolatedColor(col1, col2)));
        }
        // case 2.c: no gradients with stop less than 1.0
        else {
            XpsGradient newGrad(1.0, gradients.first().color);
            gradients.clear();
            gradients.append(newGrad);
        }
    }
}

static void addXpsGradientsToQGradient(const QList<XpsGradient> &gradients, QGradient *qgrad)
{
    for (const XpsGradient &grad : gradients) {
        qgrad->setColorAt(grad.offset, grad.color);
    }
}

static void applySpreadStyleToQGradient(const QString &style, QGradient *qgrad)
{
    if (style.isEmpty()) {
        return;
    }

    if (style == QLatin1String("Pad")) {
        qgrad->setSpread(QGradient::PadSpread);
    } else if (style == QLatin1String("Reflect")) {
        qgrad->setSpread(QGradient::ReflectSpread);
    } else if (style == QLatin1String("Repeat")) {
        qgrad->setSpread(QGradient::RepeatSpread);
    }
}

/**
    Read an UnicodeString
    \param raw the raw value of UnicodeString

    \see XPS specification 5.1.4
*/
static QString unicodeString(const QString &raw)
{
    QString ret;
    if (raw.startsWith(QLatin1String("{}"))) {
        ret = raw.mid(2);
    } else {
        ret = raw;
    }
    return ret;
}

void XpsPage::processGlyph(QPainter *painter, const XpsRenderNode &node)
{
    // TODO Currently ignored attributes: CaretStops, DeviceFontName, IsSideways, OpacityMask, Name, FixedPage.NavigateURI, xml:lang, x:key
    // TODO Indices is only partially implemented
    // TODO Currently ignored child elements: Clip, OpacityMask
    // Handled separately: RenderTransform

    QString att;

    painter->save();

    // Get font (doesn't work well because qt doesn't allow to load font from file)
    // This works despite the fact that font size isn't specified in points as required by qt. It's because I set point size to be equal to drawing unit.
    float fontSize = node.attributes.value(QStringLiteral("FontRenderingEmSize")).toFloat();
    // qCWarning(OkularXpsDebug) << "Font Rendering EmSize:" << fontSize;
    // a value of 0.0 means the text is not visible (see XPS specs, chapter 12, "Glyphs")
    if (fontSize < 0.1) {
        painter->restore();
        return;
    }
    const QString absoluteFileName = absolutePath(entryPath(fileName()), node.attributes.value(QStringLiteral("FontUri")).toString());
    QFont font = m_file->getFontByName(absoluteFileName, fontSize);
    att = node.attributes.value(QStringLiteral("StyleSimulations")).toString();
    if (!att.isEmpty()) {
        if (att == QLatin1String("ItalicSimulation")) {
            font.setItalic(true);
        } else if (att == QLatin1String("BoldSimulation")) {
            font.setBold(true);
        } else if (att == QLatin1String("BoldItalicSimulation")) {
            font.setItalic(true);
            font.setBold(true);
        }
    }
    painter->setFont(font);

    // Origin
    QPointF origin(node.attributes.value(QStringLiteral("OriginX")).toDouble(), node.attributes.value(QStringLiteral("OriginY")).toDouble());

    // Fill
    QBrush brush;
    att = node.attributes.value(QStringLiteral("Fill")).toString();
    if (att.isEmpty()) {
        QVariant data = node.getChildData(QStringLiteral("Glyphs.Fill"));
        if (data.canConvert<QBrush>()) {
            brush = data.value<QBrush>();
        } else {
            // no "Fill" attribute and no "Glyphs.Fill" child, so show nothing
            // (see XPS specs, 5.10)
            painter->restore();
            return;
        }
    } else {
        brush = parseRscRefColorForBrush(att);
        if (brush.style() > Qt::NoBrush && brush.style() < Qt::LinearGradientPattern && brush.color().alpha() == 0) {
            painter->restore();
            return;
        }
    }
    painter->setBrush(brush);
    painter->setPen(QPen(brush, 0));

    // Opacity
    att = node.attributes.value(QStringLiteral("Opacity")).toString();
    if (!att.isEmpty()) {
        bool ok = true;
        double value = att.toDouble(&ok);
        if (ok && value >= 0.1) {
            painter->setOpacity(value);
        } else {
            painter->restore();
            return;
        }
    }

    // RenderTransform
    att = node.attributes.value(QStringLiteral("RenderTransform")).toString();
    if (!att.isEmpty()) {
        painter->setWorldTransform(parseRscRefMatrix(att), true);
    }

    // Clip
    att = node.attributes.value(QStringLiteral("Clip")).toString();
    if (!att.isEmpty()) {
        QPainterPath clipPath = parseRscRefPath(att);
        if (!clipPath.isEmpty()) {
            painter->setClipPath(clipPath);
        }
    }

    // BiDiLevel - default Left-to-Right
    painter->setLayoutDirection(Qt::LeftToRight);
    att = node.attributes.value(QStringLiteral("BiDiLevel")).toString();
    if (!att.isEmpty()) {
        if ((att.toInt() % 2) == 1) {
            // odd BiDiLevel, so Right-to-Left
            painter->setLayoutDirection(Qt::RightToLeft);
        }
    }

    // Indices - partial handling only
    att = node.attributes.value(QStringLiteral("Indices")).toString();
    QList<qreal> advanceWidths;
    if (!att.isEmpty()) {
        const QStringList indicesElements = att.split(QLatin1Char(';'));
        for (const QString &indicesElement : indicesElements) {
            if (indicesElements.contains(QStringLiteral(","))) {
                const QStringList parts = indicesElement.split(QLatin1Char(','));
                if (parts.size() == 2) {
                    // regular advance case, no offsets
                    advanceWidths.append(parts.at(1).toDouble() * fontSize / 100.0);
                } else if (parts.size() == 3) {
                    // regular advance case, with uOffset
                    qreal AdvanceWidth = parts.at(1).toDouble() * fontSize / 100.0;
                    qreal uOffset = parts.at(2).toDouble() / 100.0;
                    advanceWidths.append(AdvanceWidth + uOffset);
                } else {
                    // has vertical offset, but don't know how to handle that yet
                    qCWarning(OkularXpsDebug) << "Unhandled Indices element: " << indicesElement;
                    advanceWidths.append(-1.0);
                }
            } else {
                // no special advance case
                advanceWidths.append(-1.0);
            }
        }
    }

    // UnicodeString
    QString stringToDraw(unicodeString(node.attributes.value(QStringLiteral("UnicodeString")).toString()));
    QPointF originAdvance(0, 0);
    QFontMetrics metrics = painter->fontMetrics();
    for (int i = 0; i < stringToDraw.size(); ++i) {
        QChar thisChar = stringToDraw.at(i);
        painter->drawText(origin + originAdvance, QString(thisChar));
        const qreal advanceWidth = advanceWidths.value(i, qreal(-1.0));
        if (advanceWidth > 0.0) {
            originAdvance.rx() += advanceWidth;
        } else {
            originAdvance.rx() += metrics.horizontalAdvance(thisChar);
        }
    }
    // qCWarning(OkularXpsDebug) << "Glyphs: " << atts.value("Fill") << ", " << atts.value("FontUri");
    // qCWarning(OkularXpsDebug) << "    Origin: " << atts.value("OriginX") << "," << atts.value("OriginY");
    // qCWarning(OkularXpsDebug) << "    Unicode: " << atts.value("UnicodeString");

    painter->restore();
}

void XpsPage::processFill(XpsRenderNode &node)
{
    // TODO Ignored child elements: VirtualBrush

    if (node.children.size() != 1) {
        qCWarning(OkularXpsDebug) << "Fill element should have exactly one child";
    } else {
        node.data = node.children[0].data;
    }
}

void XpsPage::processStroke(XpsRenderNode &node)
{
    // TODO Ignored child elements: VirtualBrush

    if (node.children.size() != 1) {
        qCWarning(OkularXpsDebug) << "Stroke element should have exactly one child";
    } else {
        node.data = node.children[0].data;
    }
}

void XpsPage::processImageBrush(XpsRenderNode &node)
{
    // TODO Ignored attributes: Opacity, x:key, TileMode, ViewBoxUnits, ViewPortUnits
    // TODO Check whether transformation works for non standard situations (viewbox different that whole image, Transform different that simple move & scale, Viewport different than [0, 0, 1, 1]

    QString att;
    QBrush brush;

    QRectF viewport = stringToRectF(node.attributes.value(QStringLiteral("Viewport")));
    QRectF viewbox = stringToRectF(node.attributes.value(QStringLiteral("Viewbox")));
    QImage image = loadImageFromFile(node.attributes.value(QStringLiteral("ImageSource")).toString());

    // Matrix which can transform [0, 0, 1, 1] rectangle to given viewbox
    QTransform viewboxMatrix = QTransform(viewbox.width() * image.physicalDpiX() / 96, 0, 0, viewbox.height() * image.physicalDpiY() / 96, viewbox.x(), viewbox.y());

    // Matrix which can transform [0, 0, 1, 1] rectangle to given viewport
    // TODO Take ViewPort into account
    QTransform viewportMatrix;
    att = node.attributes.value(QStringLiteral("Transform")).toString();
    if (att.isEmpty()) {
        QVariant data = node.getChildData(QStringLiteral("ImageBrush.Transform"));
        if (data.canConvert<QTransform>()) {
            viewportMatrix = data.value<QTransform>();
        } else {
            viewportMatrix = QTransform();
        }
    } else {
        viewportMatrix = parseRscRefMatrix(att);
    }
    viewportMatrix = viewportMatrix * QTransform(viewport.width(), 0, 0, viewport.height(), viewport.x(), viewport.y());

    brush = QBrush(image);
    brush.setTransform(viewboxMatrix.inverted() * viewportMatrix);

    node.data = QVariant::fromValue(brush);
}

void XpsPage::processPath(QPainter *painter, const XpsRenderNode &node)
{
    // TODO Ignored attributes: Clip, OpacityMask, StrokeEndLineCap, StorkeStartLineCap, Name, FixedPage.NavigateURI, xml:lang, x:key, AutomationProperties.Name, AutomationProperties.HelpText, SnapsToDevicePixels
    // TODO Ignored child elements: RenderTransform, Clip, OpacityMask
    // Handled separately: RenderTransform
    painter->save();

    QString att;
    QVariant data;

    // Get path
    XpsPathGeometry *pathdata = node.getChildData(QStringLiteral("Path.Data")).value<XpsPathGeometry *>();
    att = node.attributes.value(QStringLiteral("Data")).toString();
    if (!att.isEmpty()) {
        QPainterPath path = parseRscRefPath(att);
        delete pathdata;
        pathdata = new XpsPathGeometry();
        pathdata->paths.append(new XpsPathFigure(path, true));
    }
    if (!pathdata) {
        // nothing to draw
        painter->restore();
        return;
    }

    // Set Fill
    att = node.attributes.value(QStringLiteral("Fill")).toString();
    QBrush brush;
    if (!att.isEmpty()) {
        brush = parseRscRefColorForBrush(att);
    } else {
        data = node.getChildData(QStringLiteral("Path.Fill"));
        if (data.canConvert<QBrush>()) {
            brush = data.value<QBrush>();
        }
    }
    painter->setBrush(brush);

    // Stroke (pen)
    att = node.attributes.value(QStringLiteral("Stroke")).toString();
    QPen pen(Qt::transparent);
    if (!att.isEmpty()) {
        pen = parseRscRefColorForPen(att);
    } else {
        data = node.getChildData(QStringLiteral("Path.Stroke"));
        if (data.canConvert<QBrush>()) {
            pen.setBrush(data.value<QBrush>());
        }
    }
    att = node.attributes.value(QStringLiteral("StrokeThickness")).toString();
    if (!att.isEmpty()) {
        bool ok = false;
        int thickness = att.toInt(&ok);
        if (ok) {
            pen.setWidth(thickness);
        }
    }
    att = node.attributes.value(QStringLiteral("StrokeDashArray")).toString();
    if (!att.isEmpty()) {
        const QStringList pieces = att.split(QLatin1Char(' '), Qt::SkipEmptyParts);
        QList<qreal> dashPattern(pieces.count());
        bool ok = false;
        for (int i = 0; i < pieces.count(); ++i) {
            qreal value = pieces.at(i).toInt(&ok);
            if (ok) {
                dashPattern[i] = value;
            } else {
                break;
            }
        }
        if (ok) {
            pen.setDashPattern(dashPattern);
        }
    }
    att = node.attributes.value(QStringLiteral("StrokeDashOffset")).toString();
    if (!att.isEmpty()) {
        bool ok = false;
        int offset = att.toInt(&ok);
        if (ok) {
            pen.setDashOffset(offset);
        }
    }
    att = node.attributes.value(QStringLiteral("StrokeDashCap")).toString();
    if (!att.isEmpty()) {
        Qt::PenCapStyle cap = Qt::FlatCap;
        if (att == QLatin1String("Flat")) {
            cap = Qt::FlatCap;
        } else if (att == QLatin1String("Round")) {
            cap = Qt::RoundCap;
        } else if (att == QLatin1String("Square")) {
            cap = Qt::SquareCap;
        }
        // ### missing "Triangle"
        pen.setCapStyle(cap);
    }
    att = node.attributes.value(QStringLiteral("StrokeLineJoin")).toString();
    if (!att.isEmpty()) {
        Qt::PenJoinStyle joinStyle = Qt::MiterJoin;
        if (att == QLatin1String("Miter")) {
            joinStyle = Qt::MiterJoin;
        } else if (att == QLatin1String("Bevel")) {
            joinStyle = Qt::BevelJoin;
        } else if (att == QLatin1String("Round")) {
            joinStyle = Qt::RoundJoin;
        }
        pen.setJoinStyle(joinStyle);
    }
    att = node.attributes.value(QStringLiteral("StrokeMiterLimit")).toString();
    if (!att.isEmpty()) {
        bool ok = false;
        double limit = att.toDouble(&ok);
        if (ok) {
            // we have to divide it by two, as XPS consider half of the stroke width,
            // while Qt the whole of it
            pen.setMiterLimit(limit / 2);
        }
    }
    painter->setPen(pen);

    // Opacity
    att = node.attributes.value(QStringLiteral("Opacity")).toString();
    if (!att.isEmpty()) {
        painter->setOpacity(att.toDouble());
    }

    // RenderTransform
    att = node.attributes.value(QStringLiteral("RenderTransform")).toString();
    if (!att.isEmpty()) {
        painter->setWorldTransform(parseRscRefMatrix(att), true);
    }
    if (!pathdata->transform.isIdentity()) {
        painter->setWorldTransform(pathdata->transform, true);
    }

    for (const XpsPathFigure *figure : std::as_const(pathdata->paths)) {
        painter->setBrush(figure->isFilled ? brush : QBrush());
        painter->drawPath(figure->path);
    }

    delete pathdata;

    painter->restore();
}

void XpsPage::processPathData(XpsRenderNode &node)
{
    if (node.children.size() != 1) {
        qCWarning(OkularXpsDebug) << "Path.Data element should have exactly one child";
    } else {
        node.data = node.children[0].data;
    }
}

void XpsPage::processPathGeometry(XpsRenderNode &node)
{
    XpsPathGeometry *geom = new XpsPathGeometry();

    for (const XpsRenderNode &child : std::as_const(node.children)) {
        if (child.data.canConvert<XpsPathFigure *>()) {
            XpsPathFigure *figure = child.data.value<XpsPathFigure *>();
            geom->paths.append(figure);
        }
    }

    QString att;

    att = node.attributes.value(QStringLiteral("Figures")).toString();
    if (!att.isEmpty()) {
        QPainterPath path = parseRscRefPath(att);
        qDeleteAll(geom->paths);
        geom->paths.clear();
        geom->paths.append(new XpsPathFigure(path, true));
    }

    att = node.attributes.value(QStringLiteral("FillRule")).toString();
    if (!att.isEmpty()) {
        geom->fillRule = fillRuleFromString(att);
    }

    // Transform
    att = node.attributes.value(QStringLiteral("Transform")).toString();
    if (!att.isEmpty()) {
        geom->transform = parseRscRefMatrix(att);
    }

    if (!geom->paths.isEmpty()) {
        node.data = QVariant::fromValue(geom);
    } else {
        delete geom;
    }
}

void XpsPage::processPathFigure(XpsRenderNode &node)
{
    // TODO Ignored child elements: ArcSegment

    QString att;
    QPainterPath path;

    att = node.attributes.value(QStringLiteral("StartPoint")).toString();
    if (!att.isEmpty()) {
        QPointF point = getPointFromString(att);
        path.moveTo(point);
    } else {
        return;
    }

    for (const XpsRenderNode &child : std::as_const(node.children)) {
        bool isStroked = true;
        att = node.attributes.value(QStringLiteral("IsStroked")).toString();
        if (!att.isEmpty()) {
            isStroked = att == QLatin1String("true");
        }
        if (!isStroked) {
            continue;
        }

        // PolyLineSegment
        if (child.name == QLatin1String("PolyLineSegment")) {
            att = child.attributes.value(QStringLiteral("Points")).toString();
            if (!att.isEmpty()) {
                const QStringList points = att.split(QLatin1Char(' '), Qt::SkipEmptyParts);
                for (const QString &p : points) {
                    QPointF point = getPointFromString(p);
                    path.lineTo(point);
                }
            }
        }
        // PolyBezierSegment
        else if (child.name == QLatin1String("PolyBezierSegment")) {
            att = child.attributes.value(QStringLiteral("Points")).toString();
            if (!att.isEmpty()) {
                const QStringList points = att.split(QLatin1Char(' '), Qt::SkipEmptyParts);
                if (points.count() % 3 == 0) {
                    for (int i = 0; i < points.count();) {
                        QPointF firstControl = getPointFromString(points.at(i++));
                        QPointF secondControl = getPointFromString(points.at(i++));
                        QPointF endPoint = getPointFromString(points.at(i++));
                        path.cubicTo(firstControl, secondControl, endPoint);
                    }
                }
            }
        }
        // PolyQuadraticBezierSegment
        else if (child.name == QLatin1String("PolyQuadraticBezierSegment")) {
            att = child.attributes.value(QStringLiteral("Points")).toString();
            if (!att.isEmpty()) {
                const QStringList points = att.split(QLatin1Char(' '), Qt::SkipEmptyParts);
                if (points.count() % 2 == 0) {
                    for (int i = 0; i < points.count();) {
                        QPointF point1 = getPointFromString(points.at(i++));
                        QPointF point2 = getPointFromString(points.at(i++));
                        path.quadTo(point1, point2);
                    }
                }
            }
        }
    }

    bool closePath = false;
    att = node.attributes.value(QStringLiteral("IsClosed")).toString();
    if (!att.isEmpty()) {
        closePath = att == QLatin1String("true");
    }
    if (closePath) {
        path.closeSubpath();
    }

    bool isFilled = true;
    att = node.attributes.value(QStringLiteral("IsFilled")).toString();
    if (!att.isEmpty()) {
        isFilled = att == QLatin1String("true");
    }

    if (!path.isEmpty()) {
        node.data = QVariant::fromValue(new XpsPathFigure(path, isFilled));
    }
}

void XpsPage::processStartElement(QPainter *painter, const XpsRenderNode &node)
{
    if (node.name == QLatin1String("Canvas")) {
        painter->save();
        QString att = node.attributes.value(QStringLiteral("RenderTransform")).toString();
        if (!att.isEmpty()) {
            painter->setWorldTransform(parseRscRefMatrix(att), true);
        }
        att = node.attributes.value(QStringLiteral("Opacity")).toString();
        if (!att.isEmpty()) {
            double value = att.toDouble();
            if (value > 0.0 && value <= 1.0) {
                painter->setOpacity(painter->opacity() * value);
            } else {
                // setting manually to 0 is necessary to "disable"
                // all the stuff inside
                painter->setOpacity(0.0);
            }
        }
    }
}

void XpsPage::processEndElement(QPainter *painter, XpsRenderNode &node)
{
    if (node.name == QLatin1String("Glyphs")) {
        processGlyph(painter, node);
    } else if (node.name == QLatin1String("Path")) {
        processPath(painter, node);
    } else if (node.name == QLatin1String("MatrixTransform")) {
        // TODO Ignoring x:key
        node.data = QVariant::fromValue(QTransform(attsToMatrix(node.attributes.value(QStringLiteral("Matrix")).toString())));
    } else if ((node.name == QLatin1String("Canvas.RenderTransform")) || (node.name == QLatin1String("Glyphs.RenderTransform")) || (node.name == QLatin1String("Path.RenderTransform"))) {
        QVariant data = node.getRequiredChildData(QStringLiteral("MatrixTransform"));
        if (data.canConvert<QTransform>()) {
            painter->setWorldTransform(data.value<QTransform>(), true);
        }
    } else if (node.name == QLatin1String("Canvas")) {
        painter->restore();
    } else if ((node.name == QLatin1String("Path.Fill")) || (node.name == QLatin1String("Glyphs.Fill"))) {
        processFill(node);
    } else if (node.name == QLatin1String("Path.Stroke")) {
        processStroke(node);
    } else if (node.name == QLatin1String("SolidColorBrush")) {
        // TODO Ignoring opacity, x:key
        node.data = QVariant::fromValue(QBrush(QColor(hexToRgba(node.attributes.value(QStringLiteral("Color")).toLatin1()))));
    } else if (node.name == QLatin1String("ImageBrush")) {
        processImageBrush(node);
    } else if (node.name == QLatin1String("ImageBrush.Transform")) {
        node.data = node.getRequiredChildData(QStringLiteral("MatrixTransform"));
    } else if (node.name == QLatin1String("LinearGradientBrush")) {
        const XpsRenderNode *gradients = node.findChild(QStringLiteral("LinearGradientBrush.GradientStops"));
        if (gradients && gradients->data.canConvert<QGradient *>()) {
            QPointF start = getPointFromString(node.attributes.value(QStringLiteral("StartPoint")).toString());
            QPointF end = getPointFromString(node.attributes.value(QStringLiteral("EndPoint")).toString());
            QLinearGradient *qgrad = static_cast<QLinearGradient *>(gradients->data.value<QGradient *>());
            qgrad->setStart(start);
            qgrad->setFinalStop(end);
            applySpreadStyleToQGradient(node.attributes.value(QStringLiteral("SpreadMethod")).toString(), qgrad);
            node.data = QVariant::fromValue(QBrush(*qgrad));
            delete qgrad;
        }
    } else if (node.name == QLatin1String("RadialGradientBrush")) {
        const XpsRenderNode *gradients = node.findChild(QStringLiteral("RadialGradientBrush.GradientStops"));
        if (gradients && gradients->data.canConvert<QGradient *>()) {
            QPointF center = getPointFromString(node.attributes.value(QStringLiteral("Center")).toString());
            QPointF origin = getPointFromString(node.attributes.value(QStringLiteral("GradientOrigin")).toString());
            double radiusX = node.attributes.value(QStringLiteral("RadiusX")).toDouble();
            double radiusY = node.attributes.value(QStringLiteral("RadiusY")).toDouble();
            QRadialGradient *qgrad = static_cast<QRadialGradient *>(gradients->data.value<QGradient *>());
            qgrad->setCenter(center);
            qgrad->setFocalPoint(origin);
            // TODO what in case of different radii?
            qgrad->setRadius(qMin(radiusX, radiusY));
            applySpreadStyleToQGradient(node.attributes.value(QStringLiteral("SpreadMethod")).toString(), qgrad);
            node.data = QVariant::fromValue(QBrush(*qgrad));
            delete qgrad;
        }
    } else if (node.name == QLatin1String("LinearGradientBrush.GradientStops")) {
        QList<XpsGradient> gradients;
        for (const XpsRenderNode &child : std::as_const(node.children)) {
            double offset = child.attributes.value(QStringLiteral("Offset")).toDouble();
            QColor color = hexToRgba(child.attributes.value(QStringLiteral("Color")).toLatin1());
            gradients.append(XpsGradient(offset, color));
        }
        preprocessXpsGradients(gradients);
        if (!gradients.isEmpty()) {
            QLinearGradient *qgrad = new QLinearGradient();
            addXpsGradientsToQGradient(gradients, qgrad);
            node.data = QVariant::fromValue<QGradient *>(qgrad);
        }
    } else if (node.name == QLatin1String("RadialGradientBrush.GradientStops")) {
        QList<XpsGradient> gradients;
        for (const XpsRenderNode &child : std::as_const(node.children)) {
            double offset = child.attributes.value(QStringLiteral("Offset")).toDouble();
            QColor color = hexToRgba(child.attributes.value(QStringLiteral("Color")).toLatin1());
            gradients.append(XpsGradient(offset, color));
        }
        preprocessXpsGradients(gradients);
        if (!gradients.isEmpty()) {
            QRadialGradient *qgrad = new QRadialGradient();
            addXpsGradientsToQGradient(gradients, qgrad);
            node.data = QVariant::fromValue<QGradient *>(qgrad);
        }
    } else if (node.name == QLatin1String("PathFigure")) {
        processPathFigure(node);
    } else if (node.name == QLatin1String("PathGeometry")) {
        processPathGeometry(node);
    } else if (node.name == QLatin1String("Path.Data")) {
        processPathData(node);
    } else {
        // qCWarning(OkularXpsDebug) << "Unknown element: " << node->name;
    }
}

XpsPage::XpsPage(XpsFile *file, const QString &fileName)
    : m_file(file)
    , m_fileName(fileName)
    , m_pageIsRendered(false)
{
    m_pageImage = nullptr;

    // qCWarning(OkularXpsDebug) << "page file name: " << fileName;

    const KZipFileEntry *pageFile = static_cast<const KZipFileEntry *>(m_file->xpsArchive()->directory()->entry(fileName));

    QXmlStreamReader xml;
    xml.addData(readFileOrDirectoryParts(pageFile));
    while (!xml.atEnd()) {
        xml.readNext();
        if (xml.isStartElement() && (xml.name() == QStringLiteral("FixedPage"))) {
            QXmlStreamAttributes attributes = xml.attributes();
            m_pageSize.setWidth(attributes.value(QStringLiteral("Width")).toString().toDouble());
            m_pageSize.setHeight(attributes.value(QStringLiteral("Height")).toString().toDouble());
            break;
        }
    }
    if (xml.error()) {
        qCWarning(OkularXpsDebug) << "Could not parse XPS page:" << xml.errorString();
    }
}

XpsPage::~XpsPage()
{
    delete m_pageImage;
}

bool XpsPage::renderToImage(QImage *p)
{
    if ((m_pageImage == nullptr) || (m_pageImage->size() != p->size())) {
        delete m_pageImage;
        m_pageImage = new QImage(p->size(), QImage::Format_ARGB32);
        // Set one point = one drawing unit. Useful for fonts, because xps specifies font size using drawing units, not points as usual
        m_pageImage->setDotsPerMeterX(2835);
        m_pageImage->setDotsPerMeterY(2835);

        m_pageIsRendered = false;
    }
    if (!m_pageIsRendered) {
        m_pageImage->fill(qRgba(255, 255, 255, 255));
        QPainter painter(m_pageImage);
        renderToPainter(&painter);
        m_pageIsRendered = true;
    }

    *p = *m_pageImage;

    return true;
}

bool XpsPage::renderToPainter(QPainter *painter)
{
    painter->setWorldTransform(QTransform().scale((qreal)painter->device()->width() / size().width(), (qreal)painter->device()->height() / size().height()));
    const KZipFileEntry *pageFile = static_cast<const KZipFileEntry *>(m_file->xpsArchive()->directory()->entry(m_fileName));
    QByteArray data = readFileOrDirectoryParts(pageFile);
    QXmlStreamReader reader(data);

    while (!reader.atEnd()) {
        reader.readNext();
        // parse data and paint it to painter
        if (reader.isStartDocument()) {
            XpsRenderNode node;
            node.name = QStringLiteral("document");
            m_nodes.push(node);
        } else if (reader.isStartElement()) {
            XpsRenderNode node;
            node.name = reader.name().toString();
            node.attributes = reader.attributes();
            processStartElement(painter, node);
            m_nodes.push(node);
        } else if (reader.isEndElement()) {
            XpsRenderNode node = m_nodes.pop();
            if (node.name != reader.name().toString()) {
                qCWarning(OkularXpsDebug) << "Name doesn't match" << node.name << " and next from document: " << reader.name().toString();
            }
            processEndElement(painter, node);
            node.children.clear();
            m_nodes.top().children.append(node);
        }
    }

    bool ok = !reader.hasError();
    if (!ok) {
        // Error handling
    }
    qCWarning(OkularXpsDebug) << "Parse result: " << ok;

    return true;
}

QSizeF XpsPage::size() const
{
    return m_pageSize;
}

QFont XpsFile::getFontByName(const QString &absoluteFileName, float size)
{
    // qCWarning(OkularXpsDebug) << "trying to get font: " << fileName << ", size: " << size;

    int index = m_fontCache.value(absoluteFileName, -1);
    if (index == -1) {
        index = loadFontByName(absoluteFileName);
        m_fontCache[absoluteFileName] = index;
    }
    if (index == -1) {
        qCWarning(OkularXpsDebug) << "Requesting unknown font:" << absoluteFileName;
        return QFont();
    }

    const QStringList fontFamilies = QFontDatabase::applicationFontFamilies(index);
    if (fontFamilies.isEmpty()) {
        qCWarning(OkularXpsDebug) << "The unexpected has happened. No font family for a known font:" << absoluteFileName << index;
        return QFont();
    }
    const QString &fontFamily = fontFamilies[0];
    const QStringList fontStyles = QFontDatabase::styles(fontFamily);
    if (fontStyles.isEmpty()) {
        qCWarning(OkularXpsDebug) << "The unexpected has happened. No font style for a known font family:" << absoluteFileName << index << fontFamily;
        return QFont();
    }
    const QString &fontStyle = fontStyles[0];
    return QFontDatabase::font(fontFamily, fontStyle, qRound(size));
}

int XpsFile::loadFontByName(const QString &absoluteFileName)
{
    // qCWarning(OkularXpsDebug) << "font file name: " << absoluteFileName;

    const KArchiveEntry *fontFile = loadEntry(m_xpsArchive.get(), absoluteFileName, Qt::CaseInsensitive);
    if (!fontFile) {
        return -1;
    }

    QByteArray fontData = readFileOrDirectoryParts(fontFile); // once per file, according to the docs

    int result = QFontDatabase::addApplicationFontFromData(fontData);
    if (-1 == result) {
        // Try to deobfuscate font
        // TODO Use deobfuscation depending on font content type, don't do it always when standard loading fails

        const QString baseName = resourceName(absoluteFileName);

        unsigned short guid[16];
        if (!parseGUID(baseName, guid)) {
            qCWarning(OkularXpsDebug) << "File to load font - file name isn't a GUID";
        } else {
            if (fontData.length() < 32) {
                qCWarning(OkularXpsDebug) << "Font file is too small";
            } else {
                // Obfuscation - xor bytes in font binary with bytes from guid (font's filename)
                const static int mapping[] = {15, 14, 13, 12, 11, 10, 9, 8, 6, 7, 4, 5, 0, 1, 2, 3};
                for (int i = 0; i < 16; i++) {
                    fontData[i] = fontData[i] ^ guid[mapping[i]];
                    fontData[i + 16] = fontData[i + 16] ^ guid[mapping[i]];
                }
                result = QFontDatabase::addApplicationFontFromData(fontData);
            }
        }
    }

    // qCWarning(OkularXpsDebug) << "Loaded font: " << m_fontDatabase.applicationFontFamilies( result );

    return result; // a font ID
}

KZip *XpsFile::xpsArchive()
{
    return m_xpsArchive.get();
}

QImage XpsPage::loadImageFromFile(const QString &fileName)
{
    // qCWarning(OkularXpsDebug) << "image file name: " << fileName;

    if (fileName.at(0) == QLatin1Char('{')) {
        // for example: '{ColorConvertedBitmap /Resources/bla.wdp /Resources/foobar.icc}'
        // TODO: properly read a ColorConvertedBitmap
        return QImage();
    }

    QString absoluteFileName = absolutePath(entryPath(m_fileName), fileName);
    const KArchiveEntry *imageFile = loadEntry(m_file->xpsArchive(), absoluteFileName, Qt::CaseInsensitive);
    if (!imageFile) {
        // image not found
        return QImage();
    }

    /* WORKAROUND:
        XPS standard requires to use 96dpi for images which doesn't have dpi specified (in file). When Qt loads such an image,
        it sets its dpi to qt_defaultDpi and doesn't allow to find out that it happened.

        To workaround this I used this procedure: load image, set its dpi to 96, load image again. When dpi isn't set in file,
        dpi set by me stays unchanged.

        Trolltech task ID: 159527.

    */

    QImage image;
    QByteArray data = readFileOrDirectoryParts(imageFile);

    QBuffer buffer(&data);
    buffer.open(QBuffer::ReadOnly);

    QImageReader reader(&buffer);
    image = reader.read();

    image.setDotsPerMeterX(qRound(96 / 0.0254));
    image.setDotsPerMeterY(qRound(96 / 0.0254));

    buffer.seek(0);
    reader.setDevice(&buffer);
    reader.read(&image);

    return image;
}

Okular::TextPage *XpsPage::textPage()
{
    // qCWarning(OkularXpsDebug) << "Parsing XpsPage, text extraction";

    Okular::TextPage *textPage = new Okular::TextPage();

    const KZipFileEntry *pageFile = static_cast<const KZipFileEntry *>(m_file->xpsArchive()->directory()->entry(m_fileName));
    QXmlStreamReader xml;
    xml.addData(readFileOrDirectoryParts(pageFile));

    QTransform matrix = QTransform();
    QStack<QTransform> matrices;
    matrices.push(QTransform());
    bool useMatrix = false;
    QXmlStreamAttributes glyphsAtts;

    while (!xml.atEnd()) {
        xml.readNext();
        if (xml.isStartElement()) {
            if (xml.name() == QStringLiteral("Canvas")) {
                matrices.push(matrix);

                QString att = xml.attributes().value(QStringLiteral("RenderTransform")).toString();
                if (!att.isEmpty()) {
                    matrix = parseRscRefMatrix(att) * matrix;
                }
            } else if ((xml.name() == QStringLiteral("Canvas.RenderTransform")) || (xml.name() == QStringLiteral("Glyphs.RenderTransform"))) {
                useMatrix = true;
            } else if (xml.name() == QStringLiteral("MatrixTransform")) {
                if (useMatrix) {
                    matrix = attsToMatrix(xml.attributes().value(QStringLiteral("Matrix")).toString()) * matrix;
                }
            } else if (xml.name() == QStringLiteral("Glyphs")) {
                matrices.push(matrix);
                glyphsAtts = xml.attributes();
            } else if ((xml.name() == QStringLiteral("Path")) || (xml.name() == QStringLiteral("Path.Fill")) || (xml.name() == QStringLiteral("SolidColorBrush")) || (xml.name() == QStringLiteral("ImageBrush")) ||
                       (xml.name() == QStringLiteral("ImageBrush.Transform")) || (xml.name() == QStringLiteral("Path.OpacityMask")) || (xml.name() == QStringLiteral("Path.Data")) || (xml.name() == QStringLiteral("PathGeometry")) ||
                       (xml.name() == QStringLiteral("PathFigure")) || (xml.name() == QStringLiteral("PolyLineSegment"))) {
                // those are only graphical - no use in text handling
            } else if ((xml.name() == QStringLiteral("FixedPage")) || (xml.name() == QStringLiteral("FixedPage.Resources"))) {
                // not useful for text extraction
            } else {
                qCWarning(OkularXpsDebug) << "Unhandled element in Text Extraction start: " << xml.name().toString();
            }
        } else if (xml.isEndElement()) {
            if (xml.name() == QStringLiteral("Canvas")) {
                matrix = matrices.pop();
            } else if ((xml.name() == QStringLiteral("Canvas.RenderTransform")) || (xml.name() == QStringLiteral("Glyphs.RenderTransform"))) {
                useMatrix = false;
            } else if (xml.name() == QStringLiteral("MatrixTransform")) {
                // not clear if we need to do anything here yet.
            } else if (xml.name() == QStringLiteral("Glyphs")) {
                QString att = glyphsAtts.value(QStringLiteral("RenderTransform")).toString();
                if (!att.isEmpty()) {
                    matrix = parseRscRefMatrix(att) * matrix;
                }
                QString text = unicodeString(glyphsAtts.value(QStringLiteral("UnicodeString")).toString());

                // Get font (doesn't work well because qt doesn't allow to load font from file)
                const QString absoluteFileName = absolutePath(entryPath(m_fileName), glyphsAtts.value(QStringLiteral("FontUri")).toString());
                QFont font = m_file->getFontByName(absoluteFileName, glyphsAtts.value(QStringLiteral("FontRenderingEmSize")).toString().toFloat() * 72 / 96);
                QFontMetrics metrics = QFontMetrics(font);
                // Origin
                QPointF origin(glyphsAtts.value(QStringLiteral("OriginX")).toString().toDouble(), glyphsAtts.value(QStringLiteral("OriginY")).toString().toDouble());

                int lastWidth = 0;
                for (int i = 0; i < text.length(); i++) {
                    const int width = metrics.horizontalAdvance(text, i + 1);

                    Okular::NormalizedRect rect =
                        Okular::NormalizedRect((origin.x() + lastWidth) / m_pageSize.width(), (origin.y() - metrics.height()) / m_pageSize.height(), (origin.x() + width) / m_pageSize.width(), origin.y() / m_pageSize.height());
                    rect.transform(matrix);
                    textPage->append(text.mid(i, 1), rect);

                    lastWidth = width;
                }

                matrix = matrices.pop();
            } else if ((xml.name() == QStringLiteral("Path")) || (xml.name() == QStringLiteral("Path.Fill")) || (xml.name() == QStringLiteral("SolidColorBrush")) || (xml.name() == QStringLiteral("ImageBrush")) ||
                       (xml.name() == QStringLiteral("ImageBrush.Transform")) || (xml.name() == QStringLiteral("Path.OpacityMask")) || (xml.name() == QStringLiteral("Path.Data")) || (xml.name() == QStringLiteral("PathGeometry")) ||
                       (xml.name() == QStringLiteral("PathFigure")) || (xml.name() == QStringLiteral("PolyLineSegment"))) {
                // those are only graphical - no use in text handling
            } else if ((xml.name() == QStringLiteral("FixedPage")) || (xml.name() == QStringLiteral("FixedPage.Resources"))) {
                // not useful for text extraction
            } else {
                qCWarning(OkularXpsDebug) << "Unhandled element in Text Extraction end: " << xml.name().toString();
            }
        }
    }
    if (xml.error()) {
        qCWarning(OkularXpsDebug) << "Error parsing XpsPage text: " << xml.errorString();
    }
    return textPage;
}

void XpsDocument::parseDocumentStructure(const QString &documentStructureFileName)
{
    qCWarning(OkularXpsDebug) << "document structure file name: " << documentStructureFileName;
    m_haveDocumentStructure = false;

    const KZipFileEntry *documentStructureFile = static_cast<const KZipFileEntry *>(m_file->xpsArchive()->directory()->entry(documentStructureFileName));

    QXmlStreamReader xml;
    xml.addData(documentStructureFile->data());

    while (!xml.atEnd()) {
        xml.readNext();

        if (xml.isStartElement()) {
            if (xml.name() == QStringLiteral("DocumentStructure")) {
                // just a container for optional outline and story elements - nothing to do here
            } else if (xml.name() == QStringLiteral("DocumentStructure.Outline")) {
                qCWarning(OkularXpsDebug) << "found DocumentStructure.Outline";
            } else if (xml.name() == QStringLiteral("DocumentOutline")) {
                qCWarning(OkularXpsDebug) << "found DocumentOutline";
                m_docStructure = std::make_unique<Okular::DocumentSynopsis>();
            } else if (xml.name() == QStringLiteral("OutlineEntry")) {
                m_haveDocumentStructure = true;
                QXmlStreamAttributes attributes = xml.attributes();
                int outlineLevel = attributes.value(QStringLiteral("OutlineLevel")).toString().toInt();
                QString description = attributes.value(QStringLiteral("Description")).toString();
                QDomElement synopsisElement = m_docStructure->createElement(description);
                synopsisElement.setAttribute(QStringLiteral("OutlineLevel"), outlineLevel);
                QString target = attributes.value(QStringLiteral("OutlineTarget")).toString();
                int hashPosition = target.lastIndexOf(QLatin1Char('#'));
                target = target.mid(hashPosition + 1);
                // qCWarning(OkularXpsDebug) << "target: " << target;
                Okular::DocumentViewport viewport;
                viewport.pageNumber = m_docStructurePageMap.value(target);
                synopsisElement.setAttribute(QStringLiteral("Viewport"), viewport.toString());
                if (outlineLevel == 1) {
                    // qCWarning(OkularXpsDebug) << "Description: "
                    // << outlineEntryElement.attribute( "Description" );
                    m_docStructure->appendChild(synopsisElement);
                } else {
                    // find the last next highest element (so it this is level 3, we need
                    // to find the most recent level 2 node)
                    QDomNode maybeParentNode = m_docStructure->lastChild();
                    while (!maybeParentNode.isNull()) {
                        if (maybeParentNode.toElement().attribute(QStringLiteral("OutlineLevel")).toInt() == (outlineLevel - 1)) {
                            // we have the right parent
                            maybeParentNode.appendChild(synopsisElement);
                            break;
                        }
                        maybeParentNode = maybeParentNode.lastChild();
                    }
                }
            } else if (xml.name() == QStringLiteral("Story")) {
                // we need to handle Story here, but I have no idea what to do with it.
            } else if (xml.name() == QStringLiteral("StoryFragment")) {
                // we need to handle StoryFragment here, but I have no idea what to do with it.
            } else if (xml.name() == QStringLiteral("StoryFragmentReference")) {
                // we need to handle StoryFragmentReference here, but I have no idea what to do with it.
            } else {
                qCWarning(OkularXpsDebug) << "Unhandled entry in DocumentStructure: " << xml.name().toString();
            }
        }
    }
}

const Okular::DocumentSynopsis *XpsDocument::documentStructure()
{
    return m_docStructure.get();
}

bool XpsDocument::hasDocumentStructure()
{
    return m_haveDocumentStructure;
}

XpsDocument::XpsDocument(XpsFile *file, const QString &fileName)
    : m_file(file)
    , m_haveDocumentStructure(false)
    , m_docStructure(nullptr)
{
    qCWarning(OkularXpsDebug) << "document file name: " << fileName;

    const KArchiveEntry *documentEntry = file->xpsArchive()->directory()->entry(fileName);
    QString documentFilePath = fileName;
    const QString documentEntryPath = entryPath(fileName);

    QXmlStreamReader docXml;
    docXml.addData(readFileOrDirectoryParts(documentEntry, &documentFilePath));
    while (!docXml.atEnd()) {
        docXml.readNext();
        if (docXml.isStartElement()) {
            if (docXml.name() == QStringLiteral("PageContent")) {
                QString pagePath = docXml.attributes().value(QStringLiteral("Source")).toString();
                qCWarning(OkularXpsDebug) << "Page Path: " << pagePath;
                auto xpsPage = std::make_unique<XpsPage>(file, absolutePath(documentFilePath, pagePath));
                m_pages.push_back(std::move(xpsPage));
            } else if (docXml.name() == QStringLiteral("PageContent.LinkTargets")) {
                // do nothing - wait for the real LinkTarget elements
            } else if (docXml.name() == QStringLiteral("LinkTarget")) {
                QString targetName = docXml.attributes().value(QStringLiteral("Name")).toString();
                if (!targetName.isEmpty()) {
                    m_docStructurePageMap[targetName] = m_pages.size() - 1;
                }
            } else if (docXml.name() == QStringLiteral("FixedDocument")) {
                // we just ignore this - it is just a container
            } else {
                qCWarning(OkularXpsDebug) << "Unhandled entry in FixedDocument: " << docXml.name().toString();
            }
        }
    }
    if (docXml.error()) {
        qCWarning(OkularXpsDebug) << "Could not parse main XPS document file: " << docXml.errorString();
    }

    // There might be a relationships entry for this document - typically used to tell us where to find the
    // content structure description

    // We should be able to find this using a reference from some other part of the document, but I can't see it.
    const int slashPosition = fileName.lastIndexOf(QLatin1Char('/'));
    const QString documentRelationshipFile = absolutePath(documentEntryPath, QStringLiteral("_rels/") + fileName.mid(slashPosition + 1) + QStringLiteral(".rels"));

    const KZipFileEntry *relFile = static_cast<const KZipFileEntry *>(file->xpsArchive()->directory()->entry(documentRelationshipFile));

    QString documentStructureFile;
    if (relFile) {
        QXmlStreamReader xml;
        xml.addData(readFileOrDirectoryParts(relFile));
        while (!xml.atEnd()) {
            xml.readNext();
            if (xml.isStartElement() && (xml.name() == QStringLiteral("Relationship"))) {
                QXmlStreamAttributes attributes = xml.attributes();
                if (attributes.value(QStringLiteral("Type")).toString() == QLatin1String("http://schemas.microsoft.com/xps/2005/06/documentstructure")) {
                    documentStructureFile = attributes.value(QStringLiteral("Target")).toString();
                } else {
                    qCWarning(OkularXpsDebug) << "Unknown document relationships element: " << attributes.value(QStringLiteral("Type")).toString() << " : " << attributes.value(QStringLiteral("Target")).toString();
                }
            }
        }
        if (xml.error()) {
            qCWarning(OkularXpsDebug) << "Could not parse XPS page relationships file ( " << documentRelationshipFile << " ) - " << xml.errorString();
        }
    } else { // the page relationship file didn't exist in the zipfile
        // this isn't fatal
        qCWarning(OkularXpsDebug) << "Could not open Document relationship file from " << documentRelationshipFile;
    }

    if (!documentStructureFile.isEmpty()) {
        // qCWarning(OkularXpsDebug) << "Document structure filename: " << documentStructureFile;
        // make the document path absolute
        documentStructureFile = absolutePath(documentEntryPath, documentStructureFile);
        // qCWarning(OkularXpsDebug) << "Document structure absolute path: " << documentStructureFile;
        parseDocumentStructure(documentStructureFile);
    }
}

XpsDocument::~XpsDocument()
{
    m_pages.clear();
}

int XpsDocument::numPages() const
{
    return m_pages.size();
}

XpsPage *XpsDocument::page(int pageNum) const
{
    return m_pages.at(pageNum).get();
}

XpsFile::XpsFile()
{
}

XpsFile::~XpsFile()
{
    for (int fontId : std::as_const(m_fontCache)) {
        QFontDatabase::removeApplicationFont(fontId);
    }
}

bool XpsFile::loadDocument(const QString &filename)
{
    m_xpsArchive = std::make_unique<KZip>(filename);
    if (m_xpsArchive->open(QIODevice::ReadOnly) == true) {
        qCWarning(OkularXpsDebug) << "Successful open of " << m_xpsArchive->fileName();
    } else {
        qCWarning(OkularXpsDebug) << "Could not open XPS archive: " << m_xpsArchive->fileName();
        m_xpsArchive.reset();
        return false;
    }

    // The only fixed entry in XPS is /_rels/.rels
    const KArchiveEntry *relEntry = m_xpsArchive->directory()->entry(QStringLiteral("_rels/.rels"));
    if (!relEntry) {
        // this might occur if we can't read the zip directory, or it doesn't have the relationships entry
        return false;
    }

    QXmlStreamReader relXml;
    relXml.addData(readFileOrDirectoryParts(relEntry));

    QString fixedRepresentationFileName;
    // We work through the relationships document and pull out each element.
    while (!relXml.atEnd()) {
        relXml.readNext();
        if (relXml.isStartElement()) {
            if (relXml.name() == QStringLiteral("Relationship")) {
                QXmlStreamAttributes attributes = relXml.attributes();
                QString type = attributes.value(QStringLiteral("Type")).toString();
                QString target = attributes.value(QStringLiteral("Target")).toString();
                if (type == QStringLiteral("http://schemas.openxmlformats.org/package/2006/relationships/metadata/thumbnail")) {
                    m_thumbnailFileName = target;
                } else if (type == QStringLiteral("http://schemas.microsoft.com/xps/2005/06/fixedrepresentation")) {
                    fixedRepresentationFileName = target;
                } else if (type == QStringLiteral("http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties")) {
                    m_corePropertiesFileName = target;
                } else if (type == QStringLiteral("http://schemas.openxmlformats.org/package/2006/relationships/digital-signature/origin")) {
                    m_signatureOrigin = target;
                } else {
                    qCWarning(OkularXpsDebug) << "Unknown relationships element: " << type << " : " << target;
                }
            } else if (relXml.name() == QStringLiteral("Relationships")) {
                // nothing to do here - this is just the container level
            } else {
                qCWarning(OkularXpsDebug) << "unexpected element in _rels/.rels: " << relXml.name().toString();
            }
        }
    }
    if (relXml.error()) {
        qCWarning(OkularXpsDebug) << "Could not parse _rels/.rels: " << relXml.errorString();
        return false;
    }

    if (fixedRepresentationFileName.isEmpty()) {
        // FixedRepresentation is a required part of the XPS document
        return false;
    }

    const KArchiveEntry *fixedRepEntry = m_xpsArchive->directory()->entry(fixedRepresentationFileName);
    QString fixedRepresentationFilePath = fixedRepresentationFileName;

    QXmlStreamReader fixedRepXml;
    fixedRepXml.addData(readFileOrDirectoryParts(fixedRepEntry, &fixedRepresentationFileName));

    while (!fixedRepXml.atEnd()) {
        fixedRepXml.readNext();
        if (fixedRepXml.isStartElement()) {
            if (fixedRepXml.name() == QStringLiteral("DocumentReference")) {
                const QString source = fixedRepXml.attributes().value(QStringLiteral("Source")).toString();
                auto doc = std::make_unique<XpsDocument>(this, absolutePath(fixedRepresentationFilePath, source));
                for (int lv = 0; lv < doc->numPages(); ++lv) {
                    // our own copy of the pages list
                    m_pages.append(doc->page(lv));
                }
                m_documents.push_back(std::move(doc));
            } else if (fixedRepXml.name() == QStringLiteral("FixedDocumentSequence")) {
                // we don't do anything here - this is just a container for one or more DocumentReference elements
            } else {
                qCWarning(OkularXpsDebug) << "Unhandled entry in FixedDocumentSequence: " << fixedRepXml.name().toString();
            }
        }
    }
    if (fixedRepXml.error()) {
        qCWarning(OkularXpsDebug) << "Could not parse FixedRepresentation file:" << fixedRepXml.errorString();
        return false;
    }

    return true;
}

Okular::DocumentInfo XpsFile::generateDocumentInfo() const
{
    Okular::DocumentInfo docInfo;

    docInfo.set(Okular::DocumentInfo::MimeType, QStringLiteral("application/oxps"));

    if (!m_corePropertiesFileName.isEmpty()) {
        const KZipFileEntry *corepropsFile = static_cast<const KZipFileEntry *>(m_xpsArchive->directory()->entry(m_corePropertiesFileName));

        QXmlStreamReader xml;
        xml.addData(corepropsFile->data());
        while (!xml.atEnd()) {
            xml.readNext();
            if (xml.isEndElement()) {
                break;
            }
            if (xml.isStartElement()) {
                if (xml.name() == QStringLiteral("title")) {
                    docInfo.set(Okular::DocumentInfo::Title, xml.readElementText());
                } else if (xml.name() == QStringLiteral("subject")) {
                    docInfo.set(Okular::DocumentInfo::Subject, xml.readElementText());
                } else if (xml.name() == QStringLiteral("description")) {
                    docInfo.set(Okular::DocumentInfo::Description, xml.readElementText());
                } else if (xml.name() == QStringLiteral("creator")) {
                    docInfo.set(Okular::DocumentInfo::Creator, xml.readElementText());
                } else if (xml.name() == QStringLiteral("category")) {
                    docInfo.set(Okular::DocumentInfo::Category, xml.readElementText());
                } else if (xml.name() == QStringLiteral("created")) {
                    QDateTime createdDate = QDateTime::fromString(xml.readElementText(), QStringLiteral("yyyy-MM-ddThh:mm:ssZ"));
                    docInfo.set(Okular::DocumentInfo::CreationDate, QLocale().toString(createdDate, QLocale::LongFormat));
                } else if (xml.name() == QStringLiteral("modified")) {
                    QDateTime modifiedDate = QDateTime::fromString(xml.readElementText(), QStringLiteral("yyyy-MM-ddThh:mm:ssZ"));
                    docInfo.set(Okular::DocumentInfo::ModificationDate, QLocale().toString(modifiedDate, QLocale::LongFormat));
                } else if (xml.name() == QStringLiteral("keywords")) {
                    docInfo.set(Okular::DocumentInfo::Keywords, xml.readElementText());
                } else if (xml.name() == QStringLiteral("revision")) {
                    docInfo.set(QStringLiteral("revision"), xml.readElementText(), i18n("Revision"));
                }
            }
        }
        if (xml.error()) {
            qCWarning(OkularXpsDebug) << "Could not parse XPS core properties:" << xml.errorString();
        }
    } else {
        qCWarning(OkularXpsDebug) << "No core properties filename";
    }

    docInfo.set(Okular::DocumentInfo::Pages, QString::number(numPages()));

    return docInfo;
}

bool XpsFile::closeDocument()
{
    m_documents.clear();

    return true;
}

int XpsFile::numPages() const
{
    return m_pages.size();
}

int XpsFile::numDocuments() const
{
    return m_documents.size();
}

XpsDocument *XpsFile::document(int documentNum) const
{
    return m_documents.at(documentNum).get();
}

XpsPage *XpsFile::page(int pageNum) const
{
    return m_pages.at(pageNum);
}

XpsGenerator::XpsGenerator(QObject *parent, const QVariantList &args)
    : Okular::Generator(parent, args)
    , m_xpsFile(nullptr)
{
    setFeature(TextExtraction);
    setFeature(PrintNative);
    setFeature(PrintToFile);
    setFeature(Threaded);
    userMutex();
}

XpsGenerator::~XpsGenerator()
{
}

bool XpsGenerator::loadDocument(const QString &fileName, QList<Okular::Page *> &pagesVector)
{
    m_xpsFile = std::make_unique<XpsFile>();

    bool result = m_xpsFile->loadDocument(fileName);
    if (!result) {
        return false;
    }

    pagesVector.resize(m_xpsFile->numPages());

    int pagesVectorOffset = 0;

    for (int docNum = 0; docNum < m_xpsFile->numDocuments(); ++docNum) {
        const XpsDocument *doc = m_xpsFile->document(docNum);
        for (int pageNum = 0; pageNum < doc->numPages(); ++pageNum) {
            QSizeF pageSize = doc->page(pageNum)->size();
            pagesVector[pagesVectorOffset] = new Okular::Page(pagesVectorOffset, pageSize.width(), pageSize.height(), Okular::Rotation0);
            ++pagesVectorOffset;
        }
    }

    return true;
}

bool XpsGenerator::doCloseDocument()
{
    m_xpsFile->closeDocument();
    m_xpsFile.reset();

    return true;
}

QImage XpsGenerator::image(Okular::PixmapRequest *request)
{
    QMutexLocker lock(userMutex());
    QSize size((int)request->width(), (int)request->height());
    QImage image(size, QImage::Format_RGB32);
    XpsPage *pageToRender = m_xpsFile->page(request->page()->number());
    pageToRender->renderToImage(&image);
    return image;
}

Okular::TextPage *XpsGenerator::textPage(Okular::TextRequest *request)
{
    QMutexLocker lock(userMutex());
    XpsPage *xpsPage = m_xpsFile->page(request->page()->number());
    return xpsPage->textPage();
}

Okular::DocumentInfo XpsGenerator::generateDocumentInfo(const QSet<Okular::DocumentInfo::Key> &keys) const
{
    Q_UNUSED(keys);

    qCWarning(OkularXpsDebug) << "generating document metadata";

    return m_xpsFile->generateDocumentInfo();
}

const Okular::DocumentSynopsis *XpsGenerator::generateDocumentSynopsis()
{
    qCWarning(OkularXpsDebug) << "generating document synopsis";

    // we only generate the synopsis for the first file.
    if (!m_xpsFile || !m_xpsFile->document(0)) {
        return nullptr;
    }

    if (m_xpsFile->document(0)->hasDocumentStructure()) {
        return m_xpsFile->document(0)->documentStructure();
    }

    return nullptr;
}

Okular::ExportFormat::List XpsGenerator::exportFormats() const
{
    static Okular::ExportFormat::List formats;
    if (formats.isEmpty()) {
        formats.append(Okular::ExportFormat::standardFormat(Okular::ExportFormat::PlainText));
    }
    return formats;
}

bool XpsGenerator::exportTo(const QString &fileName, const Okular::ExportFormat &format)
{
    if (format.mimeType().inherits(QStringLiteral("text/plain"))) {
        QFile f(fileName);
        if (!f.open(QIODevice::WriteOnly)) {
            return false;
        }

        QTextStream ts(&f);
        for (int i = 0; i < m_xpsFile->numPages(); ++i) {
            Okular::TextPage *tp = m_xpsFile->page(i)->textPage();
            QString text = tp->text();
            ts << text;
            ts << QLatin1Char('\n');
            delete tp;
        }
        f.close();

        return true;
    }

    return false;
}

Okular::Document::PrintError XpsGenerator::print(QPrinter &printer)
{
    QList<int> pageList = Okular::FilePrinter::pageList(printer, document()->pages(), document()->currentPage() + 1, document()->bookmarkedPageList());

    QPainter painter(&printer);

    for (int i = 0; i < pageList.count(); ++i) {
        if (i != 0) {
            printer.newPage();
        }

        const int page = pageList.at(i) - 1;
        XpsPage *pageToRender = m_xpsFile->page(page);
        pageToRender->renderToPainter(&painter);
    }

    return Okular::Document::NoPrintError;
}

const XpsRenderNode *XpsRenderNode::findChild(const QString &name) const
{
    for (const XpsRenderNode &child : children) {
        if (child.name == name) {
            return &child;
        }
    }

    return nullptr;
}

QVariant XpsRenderNode::getRequiredChildData(const QString &name) const
{
    const XpsRenderNode *child = findChild(name);
    if (child == nullptr) {
        qCWarning(OkularXpsDebug) << "Required element " << name << " is missing in " << this->name;
        return QVariant();
    }

    return child->data;
}

QVariant XpsRenderNode::getChildData(const QString &name) const
{
    const XpsRenderNode *child = findChild(name);
    if (child == nullptr) {
        return QVariant();
    } else {
        return child->data;
    }
}

Q_LOGGING_CATEGORY(OkularXpsDebug, "org.kde.okular.generators.xps", QtWarningMsg)

#include "generator_xps.moc"
