//    Copyright (C) 2020-2022 Jakub Melka
//
//    This file is part of PDF4QT.
//
//    PDF4QT is free software: you can redistribute it and/or modify
//    it under the terms of the GNU Lesser General Public License as published by
//    the Free Software Foundation, either version 3 of the License, or
//    with the written consent of the copyright owner, any later version.
//
//    PDF4QT is distributed in the hope that it will be useful,
//    but WITHOUT ANY WARRANTY; without even the implied warranty of
//    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//    GNU Lesser General Public License for more details.
//
//    You should have received a copy of the GNU Lesser General Public License
//    along with PDF4QT. If not, see <https://www.gnu.org/licenses/>.

#include "pdfannotation.h"
#include "pdfdocument.h"
#include "pdfencoding.h"
#include "pdfpainter.h"
#include "pdfcms.h"
#include "pdfpagecontentprocessor.h"
#include "pdfparser.h"
#include "pdfform.h"
#include "pdfpainterutils.h"

#include <QtMath>
#include <QIcon>

#include "pdfdbgheap.h"

namespace pdf
{

PDFAnnotationBorder PDFAnnotationBorder::parseBorder(const PDFObjectStorage* storage, PDFObject object)
{
    PDFAnnotationBorder result;
    object = storage->getObject(object);

    if (object.isArray())
    {
        const PDFArray* array = object.getArray();
        if (array->getCount() >= 3)
        {
            PDFDocumentDataLoaderDecorator loader(storage);
            result.m_definition = Definition::Simple;
            result.m_hCornerRadius = loader.readNumber(array->getItem(0), 0.0);
            result.m_vCornerRadius = loader.readNumber(array->getItem(1), 0.0);
            result.m_width = loader.readNumber(array->getItem(2), 1.0);

            if (array->getCount() >= 4)
            {
                result.m_dashPattern = loader.readNumberArray(array->getItem(3));
            }
        }
    }

    return result;
}

PDFAnnotationBorder PDFAnnotationBorder::parseBS(const PDFObjectStorage* storage, PDFObject object)
{
    PDFAnnotationBorder result;

    if (const PDFDictionary* dictionary = storage->getDictionaryFromObject(object))
    {
        PDFDocumentDataLoaderDecorator loader(storage);
        result.m_definition = Definition::BorderStyle;
        result.m_width = loader.readNumberFromDictionary(dictionary, "W", 1.0);

        constexpr const std::array<std::pair<const char*, Style>, 6> styles = {
            std::pair<const char*, Style>{ "S", Style::Solid },
            std::pair<const char*, Style>{ "D", Style::Dashed },
            std::pair<const char*, Style>{ "B", Style::Beveled },
            std::pair<const char*, Style>{ "I", Style::Inset },
            std::pair<const char*, Style>{ "U", Style::Underline }
        };

        result.m_style = loader.readEnumByName(dictionary->get("S"), styles.begin(), styles.end(), Style::Solid);
    }

    return result;
}

PDFAnnotationBorderEffect PDFAnnotationBorderEffect::parse(const PDFObjectStorage* storage, PDFObject object)
{
    PDFAnnotationBorderEffect result;

    if (const PDFDictionary* dictionary = storage->getDictionaryFromObject(object))
    {
        PDFDocumentDataLoaderDecorator loader(storage);
        result.m_intensity = loader.readNumberFromDictionary(dictionary, "I", 0.0);

        constexpr const std::array<std::pair<const char*, Effect>, 2> effects = {
            std::pair<const char*, Effect>{ "S", Effect::None },
            std::pair<const char*, Effect>{ "C", Effect::Cloudy }
        };

        result.m_effect = loader.readEnumByName(dictionary->get("S"), effects.begin(), effects.end(), Effect::None);
    }

    return result;
}

PDFAppeareanceStreams PDFAppeareanceStreams::parse(const PDFObjectStorage* storage, PDFObject object)
{
    PDFAppeareanceStreams result;

    auto processSubdictionary = [&result, storage](Appearance appearance, PDFObject subdictionaryObject)
    {
        subdictionaryObject = storage->getObject(subdictionaryObject);
        if (subdictionaryObject.isDictionary())
        {
            const PDFDictionary* subdictionary = storage->getDictionaryFromObject(subdictionaryObject);
            for (size_t i = 0; i < subdictionary->getCount(); ++i)
            {
                result.m_appearanceStreams[std::make_pair(appearance, subdictionary->getKey(i).getString())] = subdictionary->getValue(i);
            }
        }
        else if (!subdictionaryObject.isNull())
        {
            result.m_appearanceStreams[std::make_pair(appearance, QByteArray())] = subdictionaryObject;
        }
    };

    if (const PDFDictionary* dictionary = storage->getDictionaryFromObject(object))
    {
        processSubdictionary(Appearance::Normal, dictionary->get("N"));
        processSubdictionary(Appearance::Rollover, dictionary->get("R"));
        processSubdictionary(Appearance::Down, dictionary->get("D"));
    }

    return result;
}

PDFObject PDFAppeareanceStreams::getAppearance(Appearance appearance, const QByteArray& state) const
{
    Key key(appearance, state);

    auto it = m_appearanceStreams.find(key);
    if (it == m_appearanceStreams.cend() && appearance != Appearance::Normal)
    {
        key.first = Appearance::Normal;
        it = m_appearanceStreams.find(key);
    }
    if (it == m_appearanceStreams.cend() && !state.isEmpty())
    {
        key.second = QByteArray();
        it = m_appearanceStreams.find(key);
    }

    if (it != m_appearanceStreams.cend())
    {
        return it->second;
    }

    return PDFObject();
}

QByteArrayList PDFAppeareanceStreams::getAppearanceStates(Appearance appearance) const
{
    QByteArrayList result;

    for (const auto& item : m_appearanceStreams)
    {
        if (item.first.first != appearance)
        {
            continue;
        }

        result << item.first.second;
    }

    return result;
}

std::vector<PDFAppeareanceStreams::Key> PDFAppeareanceStreams::getAppearanceKeys() const
{
    std::vector<Key> result;
    std::transform(m_appearanceStreams.cbegin(), m_appearanceStreams.cend(), std::back_inserter(result), [](const auto& item) { return item.first; });
    return result;
}

PDFAnnotation::PDFAnnotation() :
    m_flags(),
    m_structParent(0)
{

}

QString PDFAnnotation::getGUICaption() const
{
    QStringList texts;

    switch (getType())
    {
    case pdf::AnnotationType::Text:
        texts << PDFTranslationContext::tr("Text");
        break;
    case pdf::AnnotationType::Link:
        texts << PDFTranslationContext::tr("Line");
        break;
    case pdf::AnnotationType::FreeText:
        texts << PDFTranslationContext::tr("Free Text");
        break;
    case pdf::AnnotationType::Line:
        texts << PDFTranslationContext::tr("Line");
        break;
    case pdf::AnnotationType::Square:
        texts << PDFTranslationContext::tr("Square");
        break;
    case pdf::AnnotationType::Circle:
        texts << PDFTranslationContext::tr("Circle");
        break;
    case pdf::AnnotationType::Polygon:
        texts << PDFTranslationContext::tr("Polygon");
        break;
    case pdf::AnnotationType::Polyline:
        texts << PDFTranslationContext::tr("Polyline");
        break;
    case pdf::AnnotationType::Highlight:
        texts << PDFTranslationContext::tr("Highlight");
        break;
    case pdf::AnnotationType::Underline:
        texts << PDFTranslationContext::tr("Underline");
        break;
    case pdf::AnnotationType::Squiggly:
        texts << PDFTranslationContext::tr("Squiggly");
        break;
    case pdf::AnnotationType::StrikeOut:
        texts << PDFTranslationContext::tr("Strike Out");
        break;
    case pdf::AnnotationType::Stamp:
        texts << PDFTranslationContext::tr("Stamp");
        break;
    case pdf::AnnotationType::Caret:
        texts << PDFTranslationContext::tr("Caret");
        break;
    case pdf::AnnotationType::Ink:
        texts << PDFTranslationContext::tr("Ink");
        break;
    case pdf::AnnotationType::Popup:
        texts << PDFTranslationContext::tr("Popup");
        break;
    case pdf::AnnotationType::FileAttachment:
        texts << PDFTranslationContext::tr("File Attachment");
        break;
    case pdf::AnnotationType::Sound:
        texts << PDFTranslationContext::tr("Sound");
        break;
    case pdf::AnnotationType::Movie:
        texts << PDFTranslationContext::tr("Movie");
        break;
    case pdf::AnnotationType::Widget:
        texts << PDFTranslationContext::tr("Widget");
        break;
    case pdf::AnnotationType::Screen:
        texts << PDFTranslationContext::tr("Screen");
        break;
    case pdf::AnnotationType::PrinterMark:
        texts << PDFTranslationContext::tr("Printer Mark");
        break;
    case pdf::AnnotationType::TrapNet:
        texts << PDFTranslationContext::tr("Trap Net");
        break;
    case pdf::AnnotationType::Watermark:
        texts << PDFTranslationContext::tr("Watermark");
        break;
    case pdf::AnnotationType::Redact:
        texts << PDFTranslationContext::tr("Redaction");
        break;
    case pdf::AnnotationType::Projection:
        texts << PDFTranslationContext::tr("Projection");
        break;
    case pdf::AnnotationType::_3D:
        texts << PDFTranslationContext::tr("3D");
        break;
    case pdf::AnnotationType::RichMedia:
        texts << PDFTranslationContext::tr("Rich Media");
        break;

    default:
        break;
    }

    if (isReplyTo())
    {
        texts << PDFTranslationContext::tr("Reply");
    }

    if (!getContents().isEmpty())
    {
        texts << getContents();
    }

    return texts.join(" | ");
}

void PDFAnnotation::draw(AnnotationDrawParameters& parameters) const
{
    Q_UNUSED(parameters);
}

std::vector<PDFAppeareanceStreams::Key> PDFAnnotation::getDrawKeys(const PDFFormManager* formManager) const
{
    Q_UNUSED(formManager);

    return { PDFAppeareanceStreams::Key{ PDFAppeareanceStreams::Appearance::Normal, QByteArray() } };
}

QPainter::CompositionMode PDFAnnotation::getCompositionMode() const
{
    if (PDFBlendModeInfo::isSupportedByQt(getBlendMode()))
    {
        return PDFBlendModeInfo::getCompositionModeFromBlendMode(getBlendMode());
    }

    return PDFBlendModeInfo::getCompositionModeFromBlendMode(BlendMode::Normal);
}

QPainterPath PDFAnnotation::parsePath(const PDFObjectStorage* storage, const PDFDictionary* dictionary, bool closePath)
{
    QPainterPath path;

    PDFDocumentDataLoaderDecorator loader(storage);
    PDFObject pathObject = storage->getObject(dictionary->get("Path"));
    if (pathObject.isArray())
    {
        for (const PDFObject& pathItemObject : *pathObject.getArray())
        {
            std::vector<PDFReal> pathItem = loader.readNumberArray(pathItemObject);
            switch (pathItem.size())
            {
                case 2:
                {
                    QPointF point(pathItem[0], pathItem[1]);
                    if (path.isEmpty())
                    {
                        path.moveTo(point);
                    }
                    else
                    {
                        path.lineTo(point);
                    }
                    break;
                }

                case 4:
                {
                    if (path.isEmpty())
                    {
                        // First path item must be 'Move to' command
                        continue;
                    }

                    path.quadTo(pathItem[0], pathItem[1], pathItem[2], pathItem[3]);
                    break;
                }

                case 6:
                {
                    if (path.isEmpty())
                    {
                        // First path item must be 'Move to' command
                        continue;
                    }

                    path.cubicTo(pathItem[0], pathItem[1], pathItem[2], pathItem[3], pathItem[4], pathItem[5]);
                    break;
                }

                default:
                    break;
            }
        }
    }

    if (closePath)
    {
        path.closeSubpath();
    }

    return path;
}

void PDFAnnotation::setLanguage(const QString& language)
{
    m_language = language;
}

void PDFAnnotation::setBlendMode(const BlendMode& blendMode)
{
    m_blendMode = blendMode;
}

void PDFAnnotation::setStrokingOpacity(const PDFReal& strokingOpacity)
{
    m_strokingOpacity = strokingOpacity;
}

void PDFAnnotation::setFillingOpacity(const PDFReal& fillingOpacity)
{
    m_fillingOpacity = fillingOpacity;
}

void PDFAnnotation::setAssociatedFiles(const PDFObject& associatedFiles)
{
    m_associatedFiles = associatedFiles;
}

void PDFAnnotation::setOptionalContentReference(const PDFObjectReference& optionalContentReference)
{
    m_optionalContentReference = optionalContentReference;
}

void PDFAnnotation::setStructParent(const PDFInteger& structParent)
{
    m_structParent = structParent;
}

void PDFAnnotation::setColor(const std::vector<PDFReal>& color)
{
    m_color = color;
}

void PDFAnnotation::setAnnotationBorder(const PDFAnnotationBorder& annotationBorder)
{
    m_annotationBorder = annotationBorder;
}

void PDFAnnotation::setAppearanceState(const QByteArray& appearanceState)
{
    m_appearanceState = appearanceState;
}

void PDFAnnotation::setAppearanceStreams(const PDFAppeareanceStreams& appearanceStreams)
{
    m_appearanceStreams = appearanceStreams;
}

void PDFAnnotation::setFlags(const Flags& flags)
{
    m_flags = flags;
}

void PDFAnnotation::setLastModifiedString(const QString& lastModifiedString)
{
    m_lastModifiedString = lastModifiedString;
}

void PDFAnnotation::setLastModified(const QDateTime& lastModified)
{
    m_lastModified = lastModified;
}

void PDFAnnotation::setName(const QString& name)
{
    m_name = name;
}

void PDFAnnotation::setPageReference(const PDFObjectReference& pageReference)
{
    m_pageReference = pageReference;
}

void PDFAnnotation::setContents(const QString& contents)
{
    m_contents = contents;
}

void PDFAnnotation::setRectangle(const QRectF& rectangle)
{
    m_rectangle = rectangle;
}

void PDFAnnotation::setSelfReference(const PDFObjectReference& selfReference)
{
    m_selfReference = selfReference;
}

PDFAnnotationPtr PDFAnnotation::parse(const PDFObjectStorage* storage, PDFObjectReference reference)
{
    PDFObject object = storage->getObjectByReference(reference);
    PDFAnnotationPtr result;

    const PDFDictionary* dictionary = storage->getDictionaryFromObject(object);
    if (!dictionary)
    {
        return result;
    }

    PDFDocumentDataLoaderDecorator loader(storage);
    QRectF annotationsRectangle = loader.readRectangle(dictionary->get("Rect"), QRectF());

    // Determine type of annotation
    QByteArray subtype = loader.readNameFromDictionary(dictionary, "Subtype");
    if (subtype == "Text")
    {
        PDFTextAnnotation* textAnnotation = new PDFTextAnnotation;
        result.reset(textAnnotation);

        textAnnotation->m_open = loader.readBooleanFromDictionary(dictionary, "Open", false);
        textAnnotation->m_iconName = loader.readNameFromDictionary(dictionary, "Name");
        textAnnotation->m_state = loader.readTextStringFromDictionary(dictionary, "State", "Unmarked");
        textAnnotation->m_stateModel = loader.readTextStringFromDictionary(dictionary, "StateModel", "Marked");
    }
    else if (subtype == "Link")
    {
        PDFLinkAnnotation* linkAnnotation = new PDFLinkAnnotation;
        result.reset(linkAnnotation);

        linkAnnotation->m_action = PDFAction::parse(storage, dictionary->get("A"));
        if (!linkAnnotation->m_action)
        {
            PDFDestination destination = PDFDestination::parse(storage, dictionary->get("Dest"));
            linkAnnotation->m_action.reset(new PDFActionGoTo(destination, PDFDestination()));
        }
        linkAnnotation->m_previousAction = PDFAction::parse(storage, dictionary->get("PA"));

        constexpr const std::array<std::pair<const char*, LinkHighlightMode>, 4> highlightMode = {
            std::pair<const char*, LinkHighlightMode>{ "N", LinkHighlightMode::None },
            std::pair<const char*, LinkHighlightMode>{ "I", LinkHighlightMode::Invert },
            std::pair<const char*, LinkHighlightMode>{ "O", LinkHighlightMode::Outline },
            std::pair<const char*, LinkHighlightMode>{ "P", LinkHighlightMode::Push }
        };

        linkAnnotation->m_highlightMode = loader.readEnumByName(dictionary->get("H"), highlightMode.begin(), highlightMode.end(), LinkHighlightMode::Invert);
        linkAnnotation->m_activationRegion = parseQuadrilaterals(storage, dictionary->get("QuadPoints"), annotationsRectangle);
    }
    else if (subtype == "FreeText")
    {
        PDFFreeTextAnnotation* freeTextAnnotation = new PDFFreeTextAnnotation;
        result.reset(freeTextAnnotation);

        constexpr const std::array<std::pair<const char*, PDFFreeTextAnnotation::Intent>, 2> intents = {
            std::pair<const char*, PDFFreeTextAnnotation::Intent>{ "FreeTextCallout", PDFFreeTextAnnotation::Intent::Callout },
            std::pair<const char*, PDFFreeTextAnnotation::Intent>{ "FreeTextTypeWriter", PDFFreeTextAnnotation::Intent::TypeWriter }
        };

        freeTextAnnotation->m_defaultAppearance = loader.readStringFromDictionary(dictionary, "DA");
        freeTextAnnotation->m_justification = static_cast<PDFFreeTextAnnotation::Justification>(loader.readIntegerFromDictionary(dictionary, "Q", 0));
        freeTextAnnotation->m_defaultStyleString = loader.readTextStringFromDictionary(dictionary, "DS", QString());
        freeTextAnnotation->m_calloutLine = PDFAnnotationCalloutLine::parse(storage, dictionary->get("CL"));
        freeTextAnnotation->m_intent = loader.readEnumByName(dictionary->get("IT"), intents.begin(), intents.end(), PDFFreeTextAnnotation::Intent::None);
        freeTextAnnotation->m_effect = PDFAnnotationBorderEffect::parse(storage, dictionary->get("BE"));

        std::vector<PDFReal> differenceRectangle = loader.readNumberArrayFromDictionary(dictionary, "RD");
        if (differenceRectangle.size() == 4)
        {
            freeTextAnnotation->m_textRectangle = annotationsRectangle.adjusted(differenceRectangle[0], differenceRectangle[1], -differenceRectangle[2], -differenceRectangle[3]);
            if (!freeTextAnnotation->m_textRectangle.isValid())
            {
                freeTextAnnotation->m_textRectangle = QRectF();
            }
        }

        std::vector<QByteArray> lineEndings = loader.readNameArrayFromDictionary(dictionary, "LE");
        if (lineEndings.size() == 2)
        {
            freeTextAnnotation->m_startLineEnding = convertNameToLineEnding(lineEndings[0]);
            freeTextAnnotation->m_endLineEnding = convertNameToLineEnding(lineEndings[1]);
        }
    }
    else if (subtype == "Line")
    {
        PDFLineAnnotation* lineAnnotation = new PDFLineAnnotation;
        result.reset(lineAnnotation);

        std::vector<PDFReal> line = loader.readNumberArrayFromDictionary(dictionary, "L");
        if (line.size() == 4)
        {
            lineAnnotation->m_line = QLineF(line[0], line[1], line[2], line[3]);
        }

        std::vector<QByteArray> lineEndings = loader.readNameArrayFromDictionary(dictionary, "LE");
        if (lineEndings.size() == 2)
        {
            lineAnnotation->m_startLineEnding = convertNameToLineEnding(lineEndings[0]);
            lineAnnotation->m_endLineEnding = convertNameToLineEnding(lineEndings[1]);
        }

        lineAnnotation->m_interiorColor = loader.readNumberArrayFromDictionary(dictionary, "IC");
        lineAnnotation->m_leaderLineLength = loader.readNumberFromDictionary(dictionary, "LL", 0.0);
        lineAnnotation->m_leaderLineExtension = loader.readNumberFromDictionary(dictionary, "LLE", 0.0);
        lineAnnotation->m_leaderLineOffset = loader.readNumberFromDictionary(dictionary, "LLO", 0.0);
        lineAnnotation->m_captionRendered = loader.readBooleanFromDictionary(dictionary, "Cap", false);
        lineAnnotation->m_intent = (loader.readNameFromDictionary(dictionary, "IT") == "LineDimension") ? PDFLineAnnotation::Intent::Dimension : PDFLineAnnotation::Intent::Arrow;
        lineAnnotation->m_captionPosition = (loader.readNameFromDictionary(dictionary, "CP") == "Top") ? PDFLineAnnotation::CaptionPosition::Top : PDFLineAnnotation::CaptionPosition::Inline;
        lineAnnotation->m_measureDictionary = storage->getObject(dictionary->get("Measure"));

        std::vector<PDFReal> captionOffset = loader.readNumberArrayFromDictionary(dictionary, "CO");
        if (captionOffset.size() == 2)
        {
            lineAnnotation->m_captionOffset = QPointF(captionOffset[0], captionOffset[1]);
        }
    }
    else if (subtype == "Square" || subtype == "Circle")
    {
        PDFSimpleGeometryAnnotation* annotation = new PDFSimpleGeometryAnnotation((subtype == "Square") ? AnnotationType::Square : AnnotationType::Circle);
        result.reset(annotation);

        annotation->m_interiorColor = loader.readNumberArrayFromDictionary(dictionary, "IC");
        annotation->m_effect = PDFAnnotationBorderEffect::parse(storage, dictionary->get("BE"));

        std::vector<PDFReal> differenceRectangle = loader.readNumberArrayFromDictionary(dictionary, "RD");
        if (differenceRectangle.size() == 4)
        {
            annotation->m_geometryRectangle = annotationsRectangle.adjusted(differenceRectangle[0], differenceRectangle[1], -differenceRectangle[2], -differenceRectangle[3]);
            if (!annotation->m_geometryRectangle.isValid())
            {
                annotation->m_geometryRectangle = QRectF();
            }
        }
    }
    else if (subtype == "Polygon" || subtype == "PolyLine")
    {
        PDFPolygonalGeometryAnnotation* annotation = new PDFPolygonalGeometryAnnotation((subtype == "Polygon") ? AnnotationType::Polygon : AnnotationType::Polyline);
        result.reset(annotation);

        std::vector<PDFReal> vertices = loader.readNumberArrayFromDictionary(dictionary, "Vertices");
        const size_t count = vertices.size() / 2;
        annotation->m_vertices.reserve(count);
        for (size_t i = 0; i < count; ++i)
        {
            annotation->m_vertices.emplace_back(vertices[2 * i], vertices[2 * i + 1]);
        }

        std::vector<QByteArray> lineEndings = loader.readNameArrayFromDictionary(dictionary, "LE");
        if (lineEndings.size() == 2)
        {
            annotation->m_startLineEnding = convertNameToLineEnding(lineEndings[0]);
            annotation->m_endLineEnding = convertNameToLineEnding(lineEndings[1]);
        }

        annotation->m_interiorColor = loader.readNumberArrayFromDictionary(dictionary, "IC");
        annotation->m_effect = PDFAnnotationBorderEffect::parse(storage, dictionary->get("BE"));

        constexpr const std::array<std::pair<const char*, PDFPolygonalGeometryAnnotation::Intent>, 3> intents = {
            std::pair<const char*, PDFPolygonalGeometryAnnotation::Intent>{ "PolygonCloud", PDFPolygonalGeometryAnnotation::Intent::Cloud },
            std::pair<const char*, PDFPolygonalGeometryAnnotation::Intent>{ "PolyLineDimension", PDFPolygonalGeometryAnnotation::Intent::Dimension },
            std::pair<const char*, PDFPolygonalGeometryAnnotation::Intent>{ "PolygonDimension", PDFPolygonalGeometryAnnotation::Intent::Dimension }
        };

        annotation->m_intent = loader.readEnumByName(dictionary->get("IT"), intents.begin(), intents.end(), PDFPolygonalGeometryAnnotation::Intent::None);
        annotation->m_measure = storage->getObject(dictionary->get("Measure"));
        annotation->m_path = parsePath(storage, dictionary, subtype == "Polygon");
    }
    else if (subtype == "Highlight" ||
             subtype == "Underline" ||
             subtype == "Squiggly" ||
             subtype == "StrikeOut")
    {
        AnnotationType type = AnnotationType::Highlight;
        if (subtype == "Underline")
        {
            type = AnnotationType::Underline;
        }
        else if (subtype == "Squiggly")
        {
            type = AnnotationType::Squiggly;
        }
        else if (subtype == "StrikeOut")
        {
            type = AnnotationType::StrikeOut;
        }

        PDFHighlightAnnotation* annotation = new PDFHighlightAnnotation(type);
        result.reset(annotation);

        annotation->m_highlightArea = parseQuadrilaterals(storage, dictionary->get("QuadPoints"), annotationsRectangle);
    }
    else if (subtype == "Caret")
    {
        PDFCaretAnnotation* annotation = new PDFCaretAnnotation();
        result.reset(annotation);

        std::vector<PDFReal> differenceRectangle = loader.readNumberArrayFromDictionary(dictionary, "RD");
        if (differenceRectangle.size() == 4)
        {
            annotation->m_caretRectangle = annotationsRectangle.adjusted(differenceRectangle[0], differenceRectangle[1], -differenceRectangle[2], -differenceRectangle[3]);
            if (!annotation->m_caretRectangle.isValid())
            {
                annotation->m_caretRectangle = QRectF();
            }
        }

        annotation->m_symbol = (loader.readNameFromDictionary(dictionary, "Sy") == "P") ? PDFCaretAnnotation::Symbol::Paragraph : PDFCaretAnnotation::Symbol::None;
    }
    else if (subtype == "Stamp")
    {
        PDFStampAnnotation* annotation = new PDFStampAnnotation();
        result.reset(annotation);

        constexpr const std::array stamps = {
            std::pair<const char*, Stamp>{ "Approved", Stamp::Approved },
            std::pair<const char*, Stamp>{ "AsIs", Stamp::AsIs },
            std::pair<const char*, Stamp>{ "Confidential", Stamp::Confidential },
            std::pair<const char*, Stamp>{ "Departmental", Stamp::Departmental },
            std::pair<const char*, Stamp>{ "Draft", Stamp::Draft },
            std::pair<const char*, Stamp>{ "Experimental", Stamp::Experimental },
            std::pair<const char*, Stamp>{ "Expired", Stamp::Expired },
            std::pair<const char*, Stamp>{ "Final", Stamp::Final },
            std::pair<const char*, Stamp>{ "ForComment", Stamp::ForComment },
            std::pair<const char*, Stamp>{ "ForPublicRelease", Stamp::ForPublicRelease },
            std::pair<const char*, Stamp>{ "NotApproved", Stamp::NotApproved },
            std::pair<const char*, Stamp>{ "NotForPublicRelease", Stamp::NotForPublicRelease },
            std::pair<const char*, Stamp>{ "Sold", Stamp::Sold },
            std::pair<const char*, Stamp>{ "TopSecret", Stamp::TopSecret }
        };

        annotation->m_stamp = loader.readEnumByName(dictionary->get("Name"), stamps.begin(), stamps.end(), Stamp::Draft);

        constexpr const std::array stampsIntents = {
            std::pair<const char*, StampIntent>{ "Stamp", StampIntent::Stamp },
            std::pair<const char*, StampIntent>{ "StampImage", StampIntent::StampImage },
            std::pair<const char*, StampIntent>{ "StampSnapshot", StampIntent::StampSnapshot },
        };

        annotation->m_intent = loader.readEnumByName(dictionary->get("IT"), stampsIntents.begin(), stampsIntents.end(), StampIntent::Stamp);
    }
    else if (subtype == "Ink")
    {
        PDFInkAnnotation* annotation = new PDFInkAnnotation();
        result.reset(annotation);

        annotation->m_inkPath = parsePath(storage, dictionary, false);
        if (annotation->m_inkPath.isEmpty())
        {
            PDFObject inkList = storage->getObject(dictionary->get("InkList"));
            if (inkList.isArray())
            {
                const PDFArray* inkListArray = inkList.getArray();
                for (size_t i = 0, count = inkListArray->getCount(); i < count; ++i)
                {
                    std::vector<PDFReal> points = loader.readNumberArray(inkListArray->getItem(i));
                    const size_t pointCount = points.size() / 2;

                    for (size_t j = 0; j < pointCount; ++j)
                    {
                        QPointF point(points[j * 2], points[j * 2 + 1]);

                        if (j == 0)
                        {
                            annotation->m_inkPath.moveTo(point);
                        }
                        else
                        {
                            annotation->m_inkPath.lineTo(point);
                        }
                    }
                }
            }
        }
    }
    else if (subtype == "Popup")
    {
        PDFPopupAnnotation* annotation = new PDFPopupAnnotation();
        result.reset(annotation);

        annotation->m_opened = loader.readBooleanFromDictionary(dictionary, "Open", false);
    }
    else if (subtype == "FileAttachment")
    {
        PDFFileAttachmentAnnotation* annotation = new PDFFileAttachmentAnnotation();
        result.reset(annotation);

        annotation->m_fileSpecification = PDFFileSpecification::parse(storage, dictionary->get("FS"));

        constexpr const std::array<std::pair<const char*, FileAttachmentIcon>, 4> icons = {
            std::pair<const char*, FileAttachmentIcon>{ "Graph", FileAttachmentIcon::Graph },
            std::pair<const char*, FileAttachmentIcon>{ "Paperclip", FileAttachmentIcon::Paperclip },
            std::pair<const char*, FileAttachmentIcon>{ "PushPin", FileAttachmentIcon::PushPin },
            std::pair<const char*, FileAttachmentIcon>{ "Tag", FileAttachmentIcon::Tag }
        };

        annotation->m_icon = loader.readEnumByName(dictionary->get("Name"), icons.begin(), icons.end(), FileAttachmentIcon::PushPin);
    }
    else if (subtype == "Redact")
    {
        PDFRedactAnnotation* annotation = new PDFRedactAnnotation();
        result.reset(annotation);

        annotation->m_redactionRegion = parseQuadrilaterals(storage, dictionary->get("QuadPoints"), annotationsRectangle);
        annotation->m_interiorColor = loader.readNumberArrayFromDictionary(dictionary, "IC");
        annotation->m_overlayForm = dictionary->get("RO");
        annotation->m_overlayText = loader.readTextStringFromDictionary(dictionary, "OverlayText", QString());
        annotation->m_repeat = loader.readBooleanFromDictionary(dictionary, "Repeat", false);
        annotation->m_defaultAppearance = loader.readStringFromDictionary(dictionary, "DA");
        annotation->m_justification = loader.readIntegerFromDictionary(dictionary, "Q", 0);
    }
    else if (subtype == "Sound")
    {
        PDFSoundAnnotation* annotation = new PDFSoundAnnotation();
        result.reset(annotation);

        annotation->m_sound = PDFSound::parse(storage, dictionary->get("Sound"));

        constexpr const std::array<std::pair<const char*, PDFSoundAnnotation::Icon>, 2> icons = {
            std::pair<const char*, PDFSoundAnnotation::Icon>{ "Speaker", PDFSoundAnnotation::Icon::Speaker },
            std::pair<const char*, PDFSoundAnnotation::Icon>{ "Mic", PDFSoundAnnotation::Icon::Microphone }
        };

        annotation->m_icon = loader.readEnumByName(dictionary->get("Name"), icons.begin(), icons.end(), PDFSoundAnnotation::Icon::Speaker);
    }
    else if (subtype == "Movie")
    {
        PDFMovieAnnotation* annotation = new PDFMovieAnnotation();
        result.reset(annotation);

        annotation->m_movieTitle = loader.readStringFromDictionary(dictionary, "T");
        annotation->m_movie = PDFMovie::parse(storage, dictionary->get("Movie"));

        PDFObject activation = storage->getObject(dictionary->get("A"));
        if (activation.isBool())
        {
            annotation->m_playMovie = activation.getBool();
        }
        else if (activation.isDictionary())
        {
            annotation->m_playMovie = true;
            annotation->m_movieActivation = PDFMovieActivation::parse(storage, activation);
        }
    }
    else if (subtype == "Screen")
    {
        PDFScreenAnnotation* annotation = new PDFScreenAnnotation();
        result.reset(annotation);

        annotation->m_screenTitle = loader.readTextStringFromDictionary(dictionary, "T", QString());
        annotation->m_appearanceCharacteristics = PDFAnnotationAppearanceCharacteristics::parse(storage, dictionary->get("MK"));
        annotation->m_action = PDFAction::parse(storage, dictionary->get("A"));
        annotation->m_additionalActions = PDFAnnotationAdditionalActions::parse(storage, dictionary->get("AA"), dictionary->get("A"));
    }
    else if (subtype == "Widget")
    {
        PDFWidgetAnnotation* annotation = new PDFWidgetAnnotation();
        result.reset(annotation);

        constexpr const std::array<std::pair<const char*, PDFWidgetAnnotation::HighlightMode>, 5> highlightModes = {
            std::pair<const char*, PDFWidgetAnnotation::HighlightMode>{ "N", PDFWidgetAnnotation::HighlightMode::None },
            std::pair<const char*, PDFWidgetAnnotation::HighlightMode>{ "I", PDFWidgetAnnotation::HighlightMode::Invert },
            std::pair<const char*, PDFWidgetAnnotation::HighlightMode>{ "O", PDFWidgetAnnotation::HighlightMode::Outline },
            std::pair<const char*, PDFWidgetAnnotation::HighlightMode>{ "P", PDFWidgetAnnotation::HighlightMode::Push },
            std::pair<const char*, PDFWidgetAnnotation::HighlightMode>{ "T", PDFWidgetAnnotation::HighlightMode::Toggle }
        };

        annotation->m_highlightMode = loader.readEnumByName(dictionary->get("H"), highlightModes.begin(), highlightModes.end(), PDFWidgetAnnotation::HighlightMode::Invert);
        annotation->m_appearanceCharacteristics = PDFAnnotationAppearanceCharacteristics::parse(storage, dictionary->get("MK"));
        annotation->m_action = PDFAction::parse(storage, dictionary->get("A"));
        annotation->m_additionalActions = PDFAnnotationAdditionalActions::parse(storage, dictionary->get("AA"), dictionary->get("A"));
    }
    else if (subtype == "PrinterMark")
    {
        PDFPrinterMarkAnnotation* annotation = new PDFPrinterMarkAnnotation();
        result.reset(annotation);
    }
    else if (subtype == "TrapNet")
    {
        PDFTrapNetworkAnnotation* annotation = new PDFTrapNetworkAnnotation();
        result.reset(annotation);
    }
    else if (subtype == "Watermark")
    {
        PDFWatermarkAnnotation* annotation = new PDFWatermarkAnnotation();
        result.reset(annotation);

        if (const PDFDictionary* fixedPrintDictionary = storage->getDictionaryFromObject(dictionary->get("FixedPrint")))
        {
            annotation->m_matrix = loader.readMatrixFromDictionary(fixedPrintDictionary, "Matrix", QTransform());
            annotation->m_relativeHorizontalOffset = loader.readNumberFromDictionary(fixedPrintDictionary, "H", 0.0);
            annotation->m_relativeVerticalOffset = loader.readNumberFromDictionary(fixedPrintDictionary, "V", 0.0);
        }
    }
    else if (subtype == "Projection")
    {
        PDFProjectionAnnotation* annotation = new PDFProjectionAnnotation();
        result.reset(annotation);
    }
    else if (subtype == "3D")
    {
        PDF3DAnnotation* annotation = new PDF3DAnnotation();
        result.reset(annotation);

        annotation->m_stream = PDF3DStream::parse(storage, dictionary->get("3DD"));

        const std::vector<PDF3DView>& views = annotation->getStream().getViews();
        PDFObject defaultViewObject = storage->getObject(dictionary->get("DV"));
        if (defaultViewObject.isDictionary())
        {
            annotation->m_defaultView = PDF3DView::parse(storage, defaultViewObject);
        }
        else if (defaultViewObject.isInt())
        {
            PDFInteger index = defaultViewObject.getInteger();
            if (index >= 0 && index < PDFInteger(views.size()))
            {
                annotation->m_defaultView = views[index];
            }
        }
        else if (defaultViewObject.isName() && !views.empty())
        {
            QByteArray name = defaultViewObject.getString();
            if (name == "F")
            {
                annotation->m_defaultView = views.front();
            }
            else if (name == "L")
            {
                annotation->m_defaultView = views.back();
            }
        }

        annotation->m_activation = PDF3DActivation::parse(storage, dictionary->get("3DA"));
        annotation->m_interactive = loader.readBooleanFromDictionary(dictionary, "3DI", true);
        annotation->m_viewBox = loader.readRectangle(dictionary->get("3DB"), QRectF());
    }
    else if (subtype == "RichMedia")
    {
        PDFRichMediaAnnotation* annotation = new PDFRichMediaAnnotation();
        result.reset(annotation);

        annotation->m_content = PDFRichMediaContent::parse(storage, dictionary->get("RichMediaContent"));
        annotation->m_settings = PDFRichMediaSettings::parse(storage, dictionary->get("RichMediaSettings"));
    }
    else
    {
        // Fill unknown annotation
        result.reset(new PDFUnknownAnnotation());
    }

    if (!result)
    {
        // Invalid annotation type
        return result;
    }

    // Load common data for annotation
    result->m_selfReference = reference;
    result->m_rectangle = annotationsRectangle;
    result->m_contents = loader.readTextStringFromDictionary(dictionary, "Contents", QString());
    result->m_pageReference = loader.readReferenceFromDictionary(dictionary, "P");
    result->m_name = loader.readTextStringFromDictionary(dictionary, "NM", QString());

    QByteArray string = loader.readStringFromDictionary(dictionary, "M");
    result->m_lastModified = PDFEncoding::convertToDateTime(string);
    if (!result->m_lastModified.isValid())
    {
        result->m_lastModifiedString = loader.readTextStringFromDictionary(dictionary, "M", QString());
    }

    result->m_flags = Flags(static_cast<Flag>(loader.readIntegerFromDictionary(dictionary, "F", 0)));
    result->m_appearanceStreams = PDFAppeareanceStreams::parse(storage, dictionary->get("AP"));
    result->m_appearanceState = loader.readNameFromDictionary(dictionary, "AS");

    result->m_annotationBorder = PDFAnnotationBorder::parseBS(storage, dictionary->get("BS"));
    if (!result->m_annotationBorder.isValid())
    {
        result->m_annotationBorder = PDFAnnotationBorder::parseBorder(storage, dictionary->get("Border"));
    }
    result->m_color = loader.readNumberArrayFromDictionary(dictionary, "C");
    result->m_structParent = loader.readIntegerFromDictionary(dictionary, "StructParent", 0);
    result->m_optionalContentReference = loader.readReferenceFromDictionary(dictionary, "OC");
    result->m_strokingOpacity = loader.readNumberFromDictionary(dictionary, "CA", 1.0);
    result->m_fillingOpacity = loader.readNumberFromDictionary(dictionary, "ca", result->m_strokingOpacity);
    result->m_blendMode = PDFBlendModeInfo::getBlendMode(loader.readNameFromDictionary(dictionary, "BM"));

    if (result->m_blendMode == BlendMode::Invalid)
    {
        result->m_blendMode = BlendMode::Normal;
    }

    result->m_language = loader.readTextStringFromDictionary(dictionary, "Lang", QString());

    if (PDFMarkupAnnotation* markupAnnotation = result->asMarkupAnnotation())
    {
        markupAnnotation->m_windowTitle = loader.readTextStringFromDictionary(dictionary, "T", QString());
        markupAnnotation->m_popupAnnotation = loader.readReferenceFromDictionary(dictionary, "Popup");
        markupAnnotation->m_richTextString = loader.readTextStringFromDictionary(dictionary, "RC", QString());
        markupAnnotation->m_creationDate = PDFEncoding::convertToDateTime(loader.readStringFromDictionary(dictionary, "CreationDate"));
        markupAnnotation->m_inReplyTo = loader.readReferenceFromDictionary(dictionary, "IRT");
        markupAnnotation->m_subject = loader.readTextStringFromDictionary(dictionary, "Subj", QString());
        markupAnnotation->m_replyType = (loader.readNameFromDictionary(dictionary, "RT") == "Group") ? PDFMarkupAnnotation::ReplyType::Group : PDFMarkupAnnotation::ReplyType::Reply;
        markupAnnotation->m_intent = loader.readNameFromDictionary(dictionary, "IT");
        markupAnnotation->m_externalData = storage->getObject(dictionary->get("ExData"));
    }

    return result;
}

PDFAnnotationQuadrilaterals PDFAnnotation::parseQuadrilaterals(const PDFObjectStorage* storage, PDFObject quadrilateralsObject, const QRectF annotationRect)
{
    QPainterPath path;
    PDFAnnotationQuadrilaterals::Quadrilaterals quadrilaterals;

    PDFDocumentDataLoaderDecorator loader(storage);
    std::vector<PDFReal> points = loader.readNumberArray(quadrilateralsObject);
    const size_t quadrilateralCount = points.size() / 8;
    path.reserve(int(quadrilateralCount) + 5);
    quadrilaterals.reserve(quadrilateralCount);
    for (size_t i = 0; i < quadrilateralCount; ++i)
    {
        const size_t offset = i * 8;
        QPointF p1(points[offset + 0], points[offset + 1]);
        QPointF p2(points[offset + 2], points[offset + 3]);
        QPointF p3(points[offset + 4], points[offset + 5]);
        QPointF p4(points[offset + 6], points[offset + 7]);

        path.moveTo(p1);
        path.lineTo(p2);
        path.lineTo(p4);
        path.lineTo(p3);
        path.closeSubpath();

        quadrilaterals.emplace_back(PDFAnnotationQuadrilaterals::Quadrilateral{ p1, p2, p3, p4 });
    }

    if (path.isEmpty() && annotationRect.isValid())
    {
        // Jakub Melka: we are using points at the top, because PDF has inverted y axis
        // against the Qt's y axis.
        path.addRect(annotationRect);
        quadrilaterals.emplace_back(PDFAnnotationQuadrilaterals::Quadrilateral{ annotationRect.topLeft(), annotationRect.topRight(), annotationRect.bottomLeft(), annotationRect.bottomRight() });
    }

    return PDFAnnotationQuadrilaterals(qMove(path), qMove(quadrilaterals));
}

constexpr const std::array<std::pair<AnnotationLineEnding, const char*>, 10> lineEndings = {
    std::pair<AnnotationLineEnding, const char*>{ AnnotationLineEnding::None, "None" },
    std::pair<AnnotationLineEnding, const char*>{ AnnotationLineEnding::Square, "Square" },
    std::pair<AnnotationLineEnding, const char*>{ AnnotationLineEnding::Circle, "Circle" },
    std::pair<AnnotationLineEnding, const char*>{ AnnotationLineEnding::Diamond, "Diamond" },
    std::pair<AnnotationLineEnding, const char*>{ AnnotationLineEnding::OpenArrow, "OpenArrow" },
    std::pair<AnnotationLineEnding, const char*>{ AnnotationLineEnding::ClosedArrow, "ClosedArrow" },
    std::pair<AnnotationLineEnding, const char*>{ AnnotationLineEnding::Butt, "Butt" },
    std::pair<AnnotationLineEnding, const char*>{ AnnotationLineEnding::ROpenArrow, "ROpenArrow" },
    std::pair<AnnotationLineEnding, const char*>{ AnnotationLineEnding::RClosedArrow, "RClosedArrow" },
    std::pair<AnnotationLineEnding, const char*>{ AnnotationLineEnding::Slash, "Slash" }
};

AnnotationLineEnding PDFAnnotation::convertNameToLineEnding(const QByteArray& name)
{
    auto it = std::find_if(lineEndings.cbegin(), lineEndings.cend(), [&name](const auto& item) { return name == item.second; });
    if (it != lineEndings.cend())
    {
        return it->first;
    }

    return AnnotationLineEnding::None;
}

QByteArray PDFAnnotation::convertLineEndingToName(AnnotationLineEnding lineEnding)
{
    auto it = std::find_if(lineEndings.cbegin(), lineEndings.cend(), [lineEnding](const auto& item) { return lineEnding == item.first; });
    if (it != lineEndings.cend())
    {
        return it->second;
    }

    return lineEndings.front().second;
}

QColor PDFAnnotation::getStrokeColor() const
{
    return getDrawColorFromAnnotationColor(getColor(), getStrokeOpacity());
}

QColor PDFAnnotation::getFillColor() const
{
    return QColor();
}

QColor PDFAnnotation::getDrawColorFromAnnotationColor(const std::vector<PDFReal>& color, PDFReal opacity)
{
    switch (color.size())
    {
        case 1:
        {
            const PDFReal gray = color.back();
            return QColor::fromRgbF(gray, gray, gray, opacity);
        }

        case 3:
        {
            const PDFReal r = color[0];
            const PDFReal g = color[1];
            const PDFReal b = color[2];
            return QColor::fromRgbF(r, g, b, opacity);
        }

        case 4:
        {
            const PDFReal c = color[0];
            const PDFReal m = color[1];
            const PDFReal y = color[2];
            const PDFReal k = color[3];
            return QColor::fromCmykF(c, m, y, k, opacity);
        }

        default:
            break;
    }

    QColor black(Qt::black);
    black.setAlphaF(opacity);
    return black;
}

bool PDFAnnotation::isTypeEditable(AnnotationType type)
{
    switch (type)
    {
        case AnnotationType::Text:
        case AnnotationType::Link:
        case AnnotationType::FreeText:
        case AnnotationType::Line:
        case AnnotationType::Square:
        case AnnotationType::Circle:
        case AnnotationType::Polygon:
        case AnnotationType::Polyline:
        case AnnotationType::Highlight:
        case AnnotationType::Underline:
        case AnnotationType::Squiggly:
        case AnnotationType::StrikeOut:
        case AnnotationType::Stamp:
        case AnnotationType::Caret:
        case AnnotationType::Ink:
        case AnnotationType::FileAttachment:
        case AnnotationType::PrinterMark:
        case AnnotationType::Watermark:
        case AnnotationType::Redact:
            return true;

        default:
            break;
    }

    return false;
}

QPen PDFAnnotation::getPen() const
{
    QColor strokeColor = getStrokeColor();
    const PDFAnnotationBorder& border = getBorder();

    if (qFuzzyIsNull(border.getWidth()))
    {
        // No border is drawn
        return Qt::NoPen;
    }

    QPen pen(strokeColor);
    pen.setWidthF(border.getWidth());

    if (!border.getDashPattern().empty())
    {
        PDFLineDashPattern lineDashPattern(border.getDashPattern(), 0.0);
        pen.setStyle(Qt::CustomDashLine);
        pen.setDashPattern(lineDashPattern.createForQPen(pen.widthF()));
        pen.setDashOffset(lineDashPattern.getDashOffset());
    }

    return pen;
}

QBrush PDFAnnotation::getBrush() const
{
    QColor color = getFillColor();
    if (color.isValid())
    {
        return QBrush(color, Qt::SolidPattern);
    }

    return QBrush(Qt::NoBrush);
}

PDFAnnotationCalloutLine PDFAnnotationCalloutLine::parse(const PDFObjectStorage* storage, PDFObject object)
{
    PDFDocumentDataLoaderDecorator loader(storage);
    std::vector<PDFReal> points = loader.readNumberArray(object);

    switch (points.size())
    {
        case 4:
            return PDFAnnotationCalloutLine(QPointF(points[0], points[1]), QPointF(points[2], points[3]));

        case 6:
            return PDFAnnotationCalloutLine(QPointF(points[0], points[1]), QPointF(points[2], points[3]), QPointF(points[4], points[5]));

        default:
            break;
    }

    return PDFAnnotationCalloutLine();
}

PDFAnnotationAppearanceCharacteristics PDFAnnotationAppearanceCharacteristics::parse(const PDFObjectStorage* storage, PDFObject object)
{
    PDFAnnotationAppearanceCharacteristics result;

    if (const PDFDictionary* dictionary = storage->getDictionaryFromObject(object))
    {
        PDFDocumentDataLoaderDecorator loader(storage);

        result.m_rotation = loader.readIntegerFromDictionary(dictionary, "R", 0);
        result.m_borderColor = loader.readNumberArrayFromDictionary(dictionary, "BC");
        result.m_backgroundColor = loader.readNumberArrayFromDictionary(dictionary, "BG");
        result.m_normalCaption = loader.readTextStringFromDictionary(dictionary, "CA", QString());
        result.m_rolloverCaption = loader.readTextStringFromDictionary(dictionary, "RC", QString());
        result.m_downCaption = loader.readTextStringFromDictionary(dictionary, "AC", QString());
        result.m_normalIcon = storage->getObject(dictionary->get("I"));
        result.m_rolloverIcon = storage->getObject(dictionary->get("RI"));
        result.m_downIcon = storage->getObject(dictionary->get("IX"));
        result.m_iconFit = PDFAnnotationIconFitInfo::parse(storage, dictionary->get("IF"));
        result.m_pushButtonMode = static_cast<PushButtonMode>(loader.readIntegerFromDictionary(dictionary, "TP", PDFInteger(PDFAnnotationAppearanceCharacteristics::PushButtonMode::NoIcon)));
    }
    return result;
}

PDFAnnotationIconFitInfo PDFAnnotationIconFitInfo::parse(const PDFObjectStorage* storage, PDFObject object)
{
    PDFAnnotationIconFitInfo info;

    if (const PDFDictionary* dictionary = storage->getDictionaryFromObject(object))
    {
        PDFDocumentDataLoaderDecorator loader(storage);

        constexpr const std::array<std::pair<const char*, PDFAnnotationIconFitInfo::ScaleCondition>, 4> scaleConditions = {
            std::pair<const char*, PDFAnnotationIconFitInfo::ScaleCondition>{ "A", PDFAnnotationIconFitInfo::ScaleCondition::Always },
            std::pair<const char*, PDFAnnotationIconFitInfo::ScaleCondition>{ "B", PDFAnnotationIconFitInfo::ScaleCondition::ScaleBigger },
            std::pair<const char*, PDFAnnotationIconFitInfo::ScaleCondition>{ "S", PDFAnnotationIconFitInfo::ScaleCondition::ScaleSmaller },
            std::pair<const char*, PDFAnnotationIconFitInfo::ScaleCondition>{ "N", PDFAnnotationIconFitInfo::ScaleCondition::Never }
        };

        constexpr const std::array<std::pair<const char*, PDFAnnotationIconFitInfo::ScaleType>, 2> scaleTypes = {
            std::pair<const char*, PDFAnnotationIconFitInfo::ScaleType>{ "A", PDFAnnotationIconFitInfo::ScaleType::Anamorphic },
            std::pair<const char*, PDFAnnotationIconFitInfo::ScaleType>{ "P", PDFAnnotationIconFitInfo::ScaleType::Proportional }
        };

        std::vector<PDFReal> point = loader.readNumberArrayFromDictionary(dictionary, "A");
        if (point.size() != 2)
        {
            point.resize(2, 0.5);
        }

        info.m_scaleCondition = loader.readEnumByName(dictionary->get("SW"), scaleConditions.begin(), scaleConditions.end(), PDFAnnotationIconFitInfo::ScaleCondition::Always);
        info.m_scaleType = loader.readEnumByName(dictionary->get("S"), scaleTypes.begin(), scaleTypes.end(), PDFAnnotationIconFitInfo::ScaleType::Proportional);
        info.m_relativeProportionalPosition = QPointF(point[0], point[1]);
        info.m_fullBox = loader.readBooleanFromDictionary(dictionary, "FB", false);
    }

    return info;
}

PDFAnnotationAdditionalActions PDFAnnotationAdditionalActions::parse(const PDFObjectStorage* storage, PDFObject object, PDFObject defaultAction)
{
    PDFAnnotationAdditionalActions result;

    if (const PDFDictionary* dictionary = storage->getDictionaryFromObject(object))
    {
        result.m_actions[CursorEnter] = PDFAction::parse(storage, dictionary->get("E"));
        result.m_actions[CursorLeave] = PDFAction::parse(storage, dictionary->get("X"));
        result.m_actions[MousePressed] = PDFAction::parse(storage, dictionary->get("D"));
        result.m_actions[MouseReleased] = PDFAction::parse(storage, dictionary->get("U"));
        result.m_actions[FocusIn] = PDFAction::parse(storage, dictionary->get("Fo"));
        result.m_actions[FocusOut] = PDFAction::parse(storage, dictionary->get("Bl"));
        result.m_actions[PageOpened] = PDFAction::parse(storage, dictionary->get("PO"));
        result.m_actions[PageClosed] = PDFAction::parse(storage, dictionary->get("PC"));
        result.m_actions[PageShow] = PDFAction::parse(storage, dictionary->get("PV"));
        result.m_actions[PageHide] = PDFAction::parse(storage, dictionary->get("PI"));
        result.m_actions[FormFieldModified] = PDFAction::parse(storage, dictionary->get("K"));
        result.m_actions[FormFieldFormatted] = PDFAction::parse(storage, dictionary->get("F"));
        result.m_actions[FormFieldValidated] = PDFAction::parse(storage, dictionary->get("V"));
        result.m_actions[FormFieldCalculated] = PDFAction::parse(storage, dictionary->get("C"));
    }

    result.m_actions[Default] = PDFAction::parse(storage, defaultAction);
    return result;
}

PDFAnnotationManager::PDFAnnotationManager(PDFFontCache* fontCache,
                                           const PDFCMSManager* cmsManager,
                                           const PDFOptionalContentActivity* optionalActivity,
                                           PDFMeshQualitySettings meshQualitySettings,
                                           PDFRenderer::Features features,
                                           Target target,
                                           QObject* parent) :
    BaseClass(parent),
    m_document(nullptr),
    m_fontCache(fontCache),
    m_cmsManager(cmsManager),
    m_optionalActivity(optionalActivity),
    m_formManager(nullptr),
    m_meshQualitySettings(meshQualitySettings),
    m_features(features),
    m_target(target)
{

}

PDFAnnotationManager::~PDFAnnotationManager()
{

}

QTransform PDFAnnotationManager::prepareTransformations(const QTransform& pagePointToDevicePointMatrix,
                                                        QPaintDevice* device,
                                                        const PDFAnnotation::Flags annotationFlags,
                                                        const PDFPage* page,
                                                        QRectF& annotationRectangle) const
{
    // "Unrotate" user coordinate space, if NoRotate flag is set
    QTransform userSpaceToDeviceSpace = pagePointToDevicePointMatrix;
    if (annotationFlags.testFlag(PDFAnnotation::NoRotate))
    {
        PDFReal rotationAngle = 0.0;
        switch (page->getPageRotation())
        {
            case PageRotation::None:
                break;

            case PageRotation::Rotate90:
                rotationAngle = -90.0;
                break;

            case PageRotation::Rotate180:
                rotationAngle = -180.0;
                break;

            case PageRotation::Rotate270:
                rotationAngle = -270.0;
                break;

            default:
                Q_ASSERT(false);
                break;
        }

        QTransform rotationMatrix;
        rotationMatrix.rotate(-rotationAngle);
        QPointF topLeft = annotationRectangle.bottomLeft(); // Do not forget, that y is upward instead of Qt
        QPointF difference = topLeft - rotationMatrix.map(topLeft);

        QTransform finalMatrix;
        finalMatrix.translate(difference.x(), difference.y());
        finalMatrix.rotate(-rotationAngle);
        userSpaceToDeviceSpace = finalMatrix * userSpaceToDeviceSpace;
    }

    if (annotationFlags.testFlag(PDFAnnotation::NoZoom))
    {
        // Jakub Melka: we must adjust annotation rectangle to disable zoom. We calculate
        // inverse zoom as square root of absolute value of determinant of scale matrix.
        // Determinant corresponds approximately to zoom squared, and if we will have
        // unrotated matrix, and both axes are scaled by same value, then determinant will
        // be exactly zoom squared. Also, we will adjust to target device logical DPI,
        // if we, for example are using 4K, or 8K monitors.
        qreal zoom = 1.0 / qSqrt(qAbs(pagePointToDevicePointMatrix.determinant()));
        zoom = device->logicalDpiX() / 96.0;

        QRectF unzoomedRect(annotationRectangle.bottomLeft(), annotationRectangle.size() * zoom);
        unzoomedRect.translate(0, -unzoomedRect.height());
        annotationRectangle = unzoomedRect;
    }

    return userSpaceToDeviceSpace;
}

void PDFAnnotationManager::drawWidgetAnnotationHighlight(QRectF annotationRectangle,
                                                         const PDFAnnotation* annotation,
                                                         QPainter* painter,
                                                         QTransform userSpaceToDeviceSpace) const
{
    const bool isWidget = annotation->getType() == AnnotationType::Widget;
    if (m_formManager && isWidget)
    {
        // Is it a form field?
        const PDFFormManager::FormAppearanceFlags flags = m_formManager->getAppearanceFlags();
        const bool isFocused = m_formManager->isFocused(annotation->getSelfReference());
        if (isFocused || flags.testFlag(PDFFormManager::HighlightFields) || flags.testFlag(PDFFormManager::HighlightRequiredFields))
        {
            const PDFFormField* formField = m_formManager->getFormFieldForWidget(annotation->getSelfReference());
            if (!formField)
            {
                return;
            }

            // Nekreslíme zvýraznění push buttonů
            if (formField->getFieldType() == PDFFormField::FieldType::Button &&
                formField->getFlags().testFlag(PDFFormField::PushButton))
            {
                return;
            }

            QColor color;
            if (flags.testFlag(PDFFormManager::HighlightFields))
            {
                color = Qt::blue;
            }
            if (flags.testFlag(PDFFormManager::HighlightRequiredFields) && formField->getFlags().testFlag(PDFFormField::Required))
            {
                color = Qt::red;
            }
            if (isFocused)
            {
                color = Qt::yellow;
            }

            if (color.isValid())
            {
                color.setAlphaF(0.2f);

                // Draw annotation rectangle by highlight color
                QPainterPath highlightArea;
                highlightArea.addRect(annotationRectangle);
                highlightArea = userSpaceToDeviceSpace.map(highlightArea);
                painter->fillPath(highlightArea, color);
            }
        }
    }
}

void PDFAnnotationManager::drawPage(QPainter* painter,
                                    PDFInteger pageIndex,
                                    const PDFPrecompiledPage* compiledPage,
                                    PDFTextLayoutGetter& layoutGetter,
                                    const QTransform& pagePointToDevicePointMatrix,
                                    const PDFColorConvertor& convertor,
                                    QList<PDFRenderError>& errors) const
{
    Q_UNUSED(compiledPage);
    Q_UNUSED(layoutGetter);
    Q_UNUSED(convertor);

    const PDFPage* page = m_document->getCatalog()->getPage(pageIndex);
    Q_ASSERT(page);

    const PageAnnotations& annotations = getPageAnnotations(pageIndex);
    if (!annotations.isEmpty())
    {
        PDFRenderer::Features features = m_features;
        if (!features.testFlag(PDFRenderer::DisplayAnnotations))
        {
            // Annotation displaying is disabled
            return;
        }

        Q_ASSERT(m_fontCache);
        Q_ASSERT(m_cmsManager);
        Q_ASSERT(m_optionalActivity);

        int fontCacheLock = 0;
        const PDFCMSPointer cms = m_cmsManager->getCurrentCMS();
        m_fontCache->setCacheShrinkEnabled(&fontCacheLock, false);

        const PageAnnotation* annotationDrawnByEditor = nullptr;
        for (const PageAnnotation& annotation : annotations.annotations)
        {
            // If annotation draw is not enabled, then skip it
            if (!isAnnotationDrawEnabled(annotation))
            {
                continue;
            }

            if (isAnnotationDrawnByEditor(annotation))
            {
                Q_ASSERT(!annotationDrawnByEditor);
                annotationDrawnByEditor = &annotation;
                continue;
            }

            drawAnnotation(annotation, pagePointToDevicePointMatrix, page, cms.data(), false, errors, painter);
        }

        if (annotationDrawnByEditor)
        {
            drawAnnotation(*annotationDrawnByEditor, pagePointToDevicePointMatrix, page, cms.data(), true, errors, painter);
        }

        m_fontCache->setCacheShrinkEnabled(&fontCacheLock, true);
    }

    // Draw XFA form
    if (m_formManager)
    {
        m_formManager->drawXFAForm(pagePointToDevicePointMatrix, page, errors, painter);
    }
}

void PDFAnnotationManager::drawAnnotation(const PageAnnotation& annotation,
                                          const QTransform& pagePointToDevicePointMatrix,
                                          const PDFPage* page,
                                          const PDFCMS* cms,
                                          bool isEditorDrawEnabled,
                                          QList<PDFRenderError>& errors,
                                          QPainter* painter) const
{
    try
    {
        PDFObject appearanceStreamObject = m_document->getObject(getAppearanceStream(annotation));
        if (!appearanceStreamObject.isStream() || isEditorDrawEnabled)
        {
            // Object is not valid appearance stream. We will try to draw default
            // annotation appearance, but we must consider also optional content.
            // We do not draw annotation, if it is not ignored and annotation
            // has reference to optional content.

            drawAnnotationDirect(annotation, pagePointToDevicePointMatrix, page, cms, isEditorDrawEnabled, painter);
        }
        else
        {
            drawAnnotationUsingAppearanceStream(annotation, appearanceStreamObject, pagePointToDevicePointMatrix, page, cms, painter);
        }
    }
    catch (const PDFException& exception)
    {
        errors.push_back(PDFRenderError(RenderErrorType::Error, exception.getMessage()));
    }
    catch (const PDFRendererException &exception)
    {
        errors.push_back(exception.getError());
    }
}

bool PDFAnnotationManager::isAnnotationDrawEnabled(const PageAnnotation& annotation) const
{
    const PDFAnnotation::Flags annotationFlags = annotation.annotation->getEffectiveFlags();
    return !(annotationFlags.testFlag(PDFAnnotation::Hidden) || // Annotation is completely hidden
       (m_target == Target::Print && !annotationFlags.testFlag(PDFAnnotation::Print)) || // Target is print and annotation is marked as not printed
       (m_target == Target::View && annotationFlags.testFlag(PDFAnnotation::NoView)) ||  // Target is view, and annotation is disabled for screen
        annotation.annotation->isReplyTo()); // Annotation is reply to another annotation, display just the first annotation
}

bool PDFAnnotationManager::isAnnotationDrawnByEditor(const PageAnnotation& annotation) const
{
    if (!m_formManager)
    {
        return false;
    }

    if (annotation.annotation->getType() == AnnotationType::Widget)
    {
        return m_formManager->isEditorDrawEnabled(annotation.annotation->getSelfReference());
    }

    return false;
}

void PDFAnnotationManager::drawAnnotationDirect(const PageAnnotation& annotation,
                                                const QTransform& pagePointToDevicePointMatrix,
                                                const PDFPage* page,
                                                const PDFCMS* cms,
                                                bool isEditorDrawEnabled,
                                                QPainter* painter) const
{
    if (!m_features.testFlag(PDFRenderer::IgnoreOptionalContent) &&
        annotation.annotation->getOptionalContent().isValid())
    {
        PDFPainter pdfPainter(painter, m_features, pagePointToDevicePointMatrix, page, m_document, m_fontCache, cms, m_optionalActivity, m_meshQualitySettings);
        if (pdfPainter.isContentSuppressedByOC(annotation.annotation->getOptionalContent()))
        {
            return;
        }
    }

    QRectF annotationRectangle = annotation.annotation->getRectangle();
    {
        PDFPainterStateGuard guard(painter);
        painter->setRenderHint(QPainter::Antialiasing, true);
        painter->setWorldTransform(QTransform(pagePointToDevicePointMatrix), true);
        AnnotationDrawParameters parameters;
        parameters.painter = painter;
        parameters.annotation = annotation.annotation.data();
        parameters.formManager = m_formManager;
        parameters.key = std::make_pair(annotation.appearance, annotation.annotation->getAppearanceState());
        parameters.colorConvertor = cms->getColorConvertor();
        PDFRenderer::applyFeaturesToColorConvertor(m_features, parameters.colorConvertor);

        annotation.annotation->draw(parameters);

        if (parameters.boundingRectangle.isValid())
        {
            annotationRectangle = parameters.boundingRectangle;
        }
    }

    // Draw highlighting of fields, but only, if target is View,
    // we do not want to render form field highlight, when we are
    // printing to the printer.
    if (m_target == Target::View && !isEditorDrawEnabled)
    {
        PDFPainterStateGuard guard(painter);
        drawWidgetAnnotationHighlight(annotationRectangle, annotation.annotation.get(), painter, pagePointToDevicePointMatrix);
    }
}

void PDFAnnotationManager::drawAnnotationUsingAppearanceStream(const PageAnnotation& annotation,
                                                               const PDFObject& appearanceStreamObject,
                                                               const QTransform& pagePointToDevicePointMatrix,
                                                               const PDFPage* page,
                                                               const PDFCMS* cms,
                                                               QPainter* painter) const
{
    PDFDocumentDataLoaderDecorator loader(m_document);
    const PDFStream* formStream = appearanceStreamObject.getStream();
    const PDFDictionary* formDictionary = formStream->getDictionary();

    const PDFAnnotation::Flags annotationFlags = annotation.annotation->getEffectiveFlags();
    QRectF annotationRectangle = annotation.annotation->getRectangle();
    QRectF formBoundingBox = loader.readRectangle(formDictionary->get("BBox"), QRectF());
    QTransform formMatrix = loader.readMatrixFromDictionary(formDictionary, "Matrix", QTransform());
    QByteArray content = m_document->getDecodedStream(formStream);
    PDFObject resources = m_document->getObject(formDictionary->get("Resources"));
    PDFObject transparencyGroup = m_document->getObject(formDictionary->get("Group"));
    const PDFInteger formStructuralParentKey = loader.readIntegerFromDictionary(formDictionary, "StructParent", page->getStructureParentKey());

    if (formBoundingBox.isEmpty() || annotationRectangle.isEmpty())
    {
        // Form bounding box is empty
        return;
    }

    QTransform userSpaceToDeviceSpace = prepareTransformations(pagePointToDevicePointMatrix, painter->device(), annotationFlags, page, annotationRectangle);

    PDFRenderer::Features features = m_features;
    if (annotationFlags.testFlag(PDFAnnotation::NoZoom))
    {
        features.setFlag(PDFRenderer::ClipToCropBox, false);
    }

    // Jakub Melka: perform algorithm 8.1, defined in PDF 1.7 reference,
    // chapter 8.4.4 Appearance streams.

    // Step 1) - calculate transformed appearance box
    QRectF transformedAppearanceBox = formMatrix.mapRect(formBoundingBox);

    // Step 2) - calculate matrix A, which maps from form space to annotation space
    //           Matrix A transforms from transformed appearance box space to the
    //           annotation rectangle.
    const PDFReal scaleX = annotationRectangle.width() / transformedAppearanceBox.width();
    const PDFReal scaleY = annotationRectangle.height() / transformedAppearanceBox.height();
    const PDFReal translateX = annotationRectangle.left() - transformedAppearanceBox.left() * scaleX;
    const PDFReal translateY = annotationRectangle.bottom() - transformedAppearanceBox.bottom() * scaleY;
    QTransform A(scaleX, 0.0, 0.0, scaleY, translateX, translateY);

    // Step 3) - compute final matrix AA
    QTransform AA = formMatrix * A;

    bool isContentVisible = false;

    // Draw annotation
    {
        PDFPainterStateGuard guard(painter);
        PDFPainter pdfPainter(painter, features, userSpaceToDeviceSpace, page, m_document, m_fontCache, cms, m_optionalActivity, m_meshQualitySettings);
        pdfPainter.initializeProcessor();

        // Jakub Melka: we must check, that we do not display annotation disabled by optional content
        PDFObjectReference oc = annotation.annotation->getOptionalContent();
        isContentVisible = !oc.isValid() || !pdfPainter.isContentSuppressedByOC(oc);

        if (isContentVisible)
        {
            pdfPainter.processForm(AA, formBoundingBox, resources, transparencyGroup, content, formStructuralParentKey);
        }
    }

    // Draw highlighting of fields, but only, if target is View,
    // we do not want to render form field highlight, when we are
    // printing to the printer.
    if (isContentVisible && m_target == Target::View)
    {
        PDFPainterStateGuard guard(painter);
        painter->resetTransform();
        drawWidgetAnnotationHighlight(annotationRectangle, annotation.annotation.get(), painter, userSpaceToDeviceSpace);
    }
}

void PDFAnnotationManager::setDocument(const PDFModifiedDocument& document)
{
    if (m_document != document)
    {
        m_document = document;
        m_optionalActivity = document.getOptionalContentActivity();

        if (document.hasReset() || document.hasFlag(PDFModifiedDocument::Annotation))
        {
            m_pageAnnotations.clear();
        }
    }
}

PDFObject PDFAnnotationManager::getAppearanceStream(const PageAnnotation& pageAnnotation) const
{
    auto getAppearanceStream = [&pageAnnotation] (void) -> PDFObject
    {
        return pageAnnotation.annotation->getAppearanceStreams().getAppearance(pageAnnotation.appearance, pageAnnotation.annotation->getAppearanceState());
    };

    QMutexLocker lock(&m_mutex);
    return pageAnnotation.appearanceStream.get(getAppearanceStream);
}

const PDFAnnotationManager::PageAnnotations& PDFAnnotationManager::getPageAnnotations(PDFInteger pageIndex) const
{
    return const_cast<PDFAnnotationManager*>(this)->getPageAnnotations(pageIndex);
}

PDFAnnotationManager::PageAnnotations& PDFAnnotationManager::getPageAnnotations(PDFInteger pageIndex)
{
    Q_ASSERT(m_document);

    QMutexLocker lock(&m_mutex);
    auto it = m_pageAnnotations.find(pageIndex);
    if (it == m_pageAnnotations.cend())
    {
        // Create page annotations
        const PDFPage* page = m_document->getCatalog()->getPage(pageIndex);
        Q_ASSERT(page);

        PageAnnotations annotations;

        const std::vector<PDFObjectReference>& pageAnnotations = page->getAnnotations();
        annotations.annotations.reserve(pageAnnotations.size());
        for (PDFObjectReference annotationReference : pageAnnotations)
        {
            PDFAnnotationPtr annotationPtr = PDFAnnotation::parse(&m_document->getStorage(), annotationReference);
            if (annotationPtr)
            {
                PageAnnotation annotation;
                annotation.annotation = qMove(annotationPtr);
                annotations.annotations.emplace_back(qMove(annotation));
            }
        }

        it = m_pageAnnotations.insert(std::make_pair(pageIndex, qMove(annotations))).first;
    }

    return it->second;
}

bool PDFAnnotationManager::hasAnnotation(PDFInteger pageIndex) const
{
    return !getPageAnnotations(pageIndex).isEmpty();
}

bool PDFAnnotationManager::hasAnyPageAnnotation(const std::vector<PDFInteger>& pageIndices) const
{
    return std::any_of(pageIndices.cbegin(), pageIndices.cend(), std::bind(&PDFAnnotationManager::hasAnnotation, this, std::placeholders::_1));
}

bool PDFAnnotationManager::hasAnyPageAnnotation() const
{
    if (!m_document)
    {
        return false;
    }

    size_t pageCount = m_document->getCatalog()->getPageCount();
    for (size_t i = 0; i < pageCount; ++i)
    {
        if (hasAnnotation(i))
        {
            return true;
        }
    }

    return false;
}

PDFFormManager* PDFAnnotationManager::getFormManager() const
{
    return m_formManager;
}

void PDFAnnotationManager::setFormManager(PDFFormManager* formManager)
{
    m_formManager = formManager;
}

PDFRenderer::Features PDFAnnotationManager::getFeatures() const
{
    return m_features;
}

void PDFAnnotationManager::setFeatures(PDFRenderer::Features features)
{
    m_features = features;
}

PDFMeshQualitySettings PDFAnnotationManager::getMeshQualitySettings() const
{
    return m_meshQualitySettings;
}

void PDFAnnotationManager::setMeshQualitySettings(const PDFMeshQualitySettings& meshQualitySettings)
{
    m_meshQualitySettings = meshQualitySettings;
}

PDFFontCache* PDFAnnotationManager::getFontCache() const
{
    return m_fontCache;
}

void PDFAnnotationManager::setFontCache(PDFFontCache* fontCache)
{
    m_fontCache = fontCache;
}

const PDFOptionalContentActivity* PDFAnnotationManager::getOptionalActivity() const
{
    return m_optionalActivity;
}

void PDFAnnotationManager::setOptionalActivity(const PDFOptionalContentActivity* optionalActivity)
{
    m_optionalActivity = optionalActivity;
}

PDFAnnotationManager::Target PDFAnnotationManager::getTarget() const
{
    return m_target;
}

void PDFAnnotationManager::setTarget(Target target)
{
    m_target = target;
}


void PDFSimpleGeometryAnnotation::draw(AnnotationDrawParameters& parameters) const
{
    Q_ASSERT(parameters.painter);
    parameters.boundingRectangle = getRectangle();

    QPainter& painter = *parameters.painter;
    painter.setPen(getPen());
    painter.setBrush(getBrush());
    painter.setCompositionMode(getCompositionMode());

    switch (getType())
    {
        case AnnotationType::Square:
        {
            const PDFAnnotationBorder& border = getBorder();

            const PDFReal hCornerRadius = border.getHorizontalCornerRadius();
            const PDFReal vCornerRadius = border.getVerticalCornerRadius();
            const bool isRounded = !qFuzzyIsNull(hCornerRadius) || !qFuzzyIsNull(vCornerRadius);

            if (isRounded)
            {
                painter.drawRoundedRect(getRectangle(), hCornerRadius, vCornerRadius, Qt::AbsoluteSize);
            }
            else
            {
                painter.drawRect(getRectangle());
            }
            break;
        }

        case AnnotationType::Circle:
        {
            const PDFAnnotationBorder& border = getBorder();
            const PDFReal width = border.getWidth();
            QRectF rectangle = getRectangle();
            rectangle.adjust(width, width, -width, -width);
            painter.drawEllipse(rectangle);
            break;
        }

        default:
        {
            Q_ASSERT(false);
            break;
        }
    }
}

QColor PDFSimpleGeometryAnnotation::getFillColor() const
{
    return getDrawColorFromAnnotationColor(getInteriorColor(), getFillOpacity());
}

bool PDFMarkupAnnotation::isReplyTo() const
{
    return m_inReplyTo.isValid() && m_replyType == ReplyType::Reply;
}

void PDFMarkupAnnotation::setWindowTitle(const QString& windowTitle)
{
    m_windowTitle = windowTitle;
}

void PDFMarkupAnnotation::setPopupAnnotation(const PDFObjectReference& popupAnnotation)
{
    m_popupAnnotation = popupAnnotation;
}

void PDFMarkupAnnotation::setRichTextString(const QString& richTextString)
{
    m_richTextString = richTextString;
}

void PDFMarkupAnnotation::setCreationDate(const QDateTime& creationDate)
{
    m_creationDate = creationDate;
}

void PDFMarkupAnnotation::setInReplyTo(PDFObjectReference inReplyTo)
{
    m_inReplyTo = inReplyTo;
}

void PDFMarkupAnnotation::setSubject(const QString& subject)
{
    m_subject = subject;
}

void PDFMarkupAnnotation::setIntent(const QByteArray& intent)
{
    m_intent = intent;
}

void PDFMarkupAnnotation::setExternalData(const PDFObject& externalData)
{
    m_externalData = externalData;
}

void PDFMarkupAnnotation::setReplyType(ReplyType replyType)
{
    m_replyType = replyType;
}

std::vector<PDFAppeareanceStreams::Key> PDFTextAnnotation::getDrawKeys(const PDFFormManager* formManager) const
{
    Q_UNUSED(formManager);

    return { PDFAppeareanceStreams::Key{ PDFAppeareanceStreams::Appearance::Normal, QByteArray() },
             PDFAppeareanceStreams::Key{ PDFAppeareanceStreams::Appearance::Rollover, QByteArray() },
             PDFAppeareanceStreams::Key{ PDFAppeareanceStreams::Appearance::Down, QByteArray() } };
}

void PDFTextAnnotation::draw(AnnotationDrawParameters& parameters) const
{
    QColor strokeColor = QColor::fromRgbF(0.0, 0.0, 0.0, getStrokeOpacity());
    QColor fillColor = (parameters.key.first == PDFAppeareanceStreams::Appearance::Normal) ? QColor::fromRgbF(1.0, 1.0, 0.0, getFillOpacity()) :
                                                                                             QColor::fromRgbF(1.0, 0.0, 0.0, getFillOpacity());

    constexpr const PDFReal rectSize = 32.0;
    constexpr const PDFReal penWidth = 2.0;

    QPainter& painter = *parameters.painter;
    painter.setCompositionMode(getCompositionMode());
    QRectF rectangle = getRectangle();
    rectangle.setSize(QSizeF(rectSize, rectSize));

    QPen pen(strokeColor);
    pen.setWidthF(penWidth);
    painter.setPen(pen);

    painter.setBrush(QBrush(fillColor, Qt::SolidPattern));

    QRectF ellipseRectangle = rectangle;
    ellipseRectangle.adjust(penWidth, penWidth, -penWidth, -penWidth);

    // Draw the ellipse
    painter.drawEllipse(ellipseRectangle);

    QFont font = painter.font();
    font.setPixelSize(16.0);

    QString text = getTextForIcon(m_iconName);

    QPainterPath textPath;
    textPath.addText(0.0, 0.0, font, text);
    textPath = QTransform(1.0, 0.0, 0.0, -1.0, 0.0, 0.0).map(textPath);
    QRectF textBoundingRect = textPath.boundingRect();
    QPointF offset = rectangle.center() - textBoundingRect.center();
    textPath.translate(offset);
    painter.fillPath(textPath, QBrush(strokeColor, Qt::SolidPattern));

    parameters.boundingRectangle = rectangle;
}

PDFTextAnnotation::Flags PDFTextAnnotation::getEffectiveFlags() const
{
    return getFlags() | NoZoom | NoRotate;
}

QIcon PDFTextAnnotation::createIcon(QString key, QSize size)
{
    QIcon icon;

    QPixmap pixmap(size);
    pixmap.fill(Qt::transparent);

    QRect rectangle(QPoint(0, 0), size);
    rectangle.adjust(1, 1, -1, -1);

    QPainter painter(&pixmap);
    painter.setRenderHint(QPainter::Antialiasing);
    painter.setRenderHint(QPainter::TextAntialiasing);

    QString text = getTextForIcon(key);

    QFont font = painter.font();
    font.setPixelSize(size.height() * 0.75);

    QPainterPath textPath;
    textPath.addText(0.0, 0.0, font, text);
    QRectF textBoundingRect = textPath.boundingRect();
    QPointF offset = rectangle.center() - textBoundingRect.center();
    textPath.translate(offset);
    painter.fillPath(textPath, QBrush(Qt::black, Qt::SolidPattern));
    painter.end();

    icon.addPixmap(qMove(pixmap));
    return icon;
}

QString PDFTextAnnotation::getTextForIcon(const QString& key)
{
    QString text = "?";
    if (key == "Comment")
    {
        text = QString::fromUtf16(u"\U0001F4AC");
    }
    else if (key == "Help")
    {
        text = "?";
    }
    else if (key == "Insert")
    {
        text = QString::fromUtf16(u"\u2380");
    }
    else if (key == "Key")
    {
        text = QString::fromUtf16(u"\U0001F511");
    }
    else if (key == "NewParagraph")
    {
        text = QString::fromUtf16(u"\u2606");
    }
    else if (key == "Note")
    {
        text = QString::fromUtf16(u"\u266A");
    }
    else if (key == "Paragraph")
    {
        text = QString::fromUtf16(u"\u00B6");
    }
    return text;
}

void PDFLineAnnotation::draw(AnnotationDrawParameters& parameters) const
{
    QLineF line = getLine();
    if (line.isNull())
    {
        // Jakub Melka: do not draw empty lines
        return;
    }

    QPainter& painter = *parameters.painter;
    painter.setCompositionMode(getCompositionMode());
    painter.setPen(getPen());
    painter.setBrush(getBrush());

    QPainterPath boundingPath;
    boundingPath.moveTo(line.p1());
    boundingPath.lineTo(line.p2());

    LineGeometryInfo info = LineGeometryInfo::create(line);

    const PDFReal leaderLineLength = getLeaderLineLength();
    const PDFReal coefficientSigned = (leaderLineLength >= 0.0) ? 1.0 : -1.0;
    const PDFReal leaderLineOffset = getLeaderLineOffset() * coefficientSigned;
    const PDFReal leaderLineExtension = getLeaderLineExtension() * coefficientSigned;
    const PDFReal lineEndingSize = qMin(painter.pen().widthF() * 5.0, line.length() * 0.5);
    const bool hasLeaderLine = !qFuzzyIsNull(leaderLineLength) || !qFuzzyIsNull(leaderLineExtension);

    QLineF normalLine = info.transformedLine.normalVector().unitVector();
    QPointF normalVector = normalLine.p1() - normalLine.p2();

    QLineF lineToPaint = info.transformedLine;
    if (hasLeaderLine)
    {
        // We will draw leader lines at both start/end
        QPointF p1llStart = info.transformedLine.p1() + normalVector * leaderLineOffset;
        QPointF p1llEnd = info.transformedLine.p1() + normalVector * (leaderLineOffset + leaderLineLength + leaderLineExtension);

        QLineF llStart(p1llStart, p1llEnd);
        llStart = info.LCStoGCS.map(llStart);

        boundingPath.moveTo(llStart.p1());
        boundingPath.lineTo(llStart.p2());
        painter.drawLine(llStart);

        QPointF p2llStart = info.transformedLine.p2() + normalVector * leaderLineOffset;
        QPointF p2llEnd = info.transformedLine.p2() + normalVector * (leaderLineOffset + leaderLineLength + leaderLineExtension);

        QLineF llEnd(p2llStart, p2llEnd);
        llEnd = info.LCStoGCS.map(llEnd);

        boundingPath.moveTo(llEnd.p1());
        boundingPath.lineTo(llEnd.p2());
        painter.drawLine(llEnd);

        lineToPaint.translate(normalVector * (leaderLineOffset + leaderLineLength));
    }

    QLineF lineToPaintOrig = info.LCStoGCS.map(lineToPaint);
    info = LineGeometryInfo::create(lineToPaintOrig);
    drawLine(info, painter, lineEndingSize, getStartLineEnding(), getEndLineEnding(), boundingPath, getCaptionOffset(), getContents(), getCaptionPosition() == CaptionPosition::Top);

    parameters.boundingRectangle = boundingPath.boundingRect();
    parameters.boundingRectangle.adjust(-lineEndingSize, -lineEndingSize, lineEndingSize, lineEndingSize);
}

QColor PDFLineAnnotation::getFillColor() const
{
    return getDrawColorFromAnnotationColor(getInteriorColor(), getFillOpacity());
}

void PDFPolygonalGeometryAnnotation::draw(AnnotationDrawParameters& parameters) const
{
    if (m_vertices.empty())
    {
        // Jakub Melka: do not draw empty lines
        return;
    }

    QPainter& painter = *parameters.painter;
    painter.setCompositionMode(getCompositionMode());
    painter.setPen(getPen());
    painter.setBrush(getBrush());

    const PDFReal penWidth = painter.pen().widthF();
    switch (m_type)
    {
        case AnnotationType::Polygon:
        {
            if (m_path.isEmpty())
            {
                QPolygonF polygon;
                polygon.reserve(int(m_vertices.size() + 1));
                for (const QPointF& point : m_vertices)
                {
                    polygon << point;
                }
                if (!polygon.isClosed())
                {
                    polygon << m_vertices.front();
                }

                painter.drawPolygon(polygon, Qt::OddEvenFill);
                parameters.boundingRectangle = polygon.boundingRect();
                parameters.boundingRectangle.adjust(-penWidth, -penWidth, penWidth, penWidth);
            }
            else
            {
                painter.drawPath(m_path);
                parameters.boundingRectangle = m_path.boundingRect();
                parameters.boundingRectangle.adjust(-penWidth, -penWidth, penWidth, penWidth);
            }

            break;
        }

        case AnnotationType::Polyline:
        {
            const PDFReal lineEndingSize = painter.pen().widthF() * 5.0;
            QPainterPath boundingPath;

            if (m_path.isEmpty())
            {
                const size_t pointCount = m_vertices.size();
                const size_t lastPoint = pointCount - 1;
                for (size_t i = 1; i < pointCount; ++i)
                {
                    if (i == 1)
                    {
                        // We draw first line
                        drawLine(LineGeometryInfo::create(QLineF(m_vertices[i - 1], m_vertices[i])), painter, lineEndingSize, getStartLineEnding(), AnnotationLineEnding::None, boundingPath, QPointF(), QString(), true);
                    }
                    else if (i == lastPoint)
                    {
                        // We draw last line
                        drawLine(LineGeometryInfo::create(QLineF(m_vertices[i - 1], m_vertices[i])), painter, lineEndingSize, AnnotationLineEnding::None, getEndLineEnding(), boundingPath, QPointF(), QString(), true);
                    }
                    else
                    {
                        QLineF line(m_vertices[i - 1], m_vertices[i]);
                        boundingPath.moveTo(line.p1());
                        boundingPath.lineTo(line.p2());
                        painter.drawLine(line);
                    }
                }
            }
            else
            {
                const PDFReal angle = 30;
                const PDFReal lineEndingHalfSize = lineEndingSize * 0.5;
                const PDFReal arrowAxisLength = lineEndingHalfSize / qTan(qDegreesToRadians(angle));

                boundingPath = m_path;
                painter.drawPath(m_path);

                QTransform LCStoGCS_start = LineGeometryInfo::create(QLineF(m_path.pointAtPercent(0.00), m_path.pointAtPercent(0.01))).LCStoGCS;
                QTransform LCStoGCS_end = LineGeometryInfo::create(QLineF(m_path.pointAtPercent(0.99), m_path.pointAtPercent(1.00))).LCStoGCS;

                drawLineEnding(&painter, m_path.pointAtPercent(0), lineEndingSize, arrowAxisLength, getStartLineEnding(), false, LCStoGCS_start, boundingPath);
                drawLineEnding(&painter, m_path.pointAtPercent(1), lineEndingSize, arrowAxisLength, getEndLineEnding(), true, LCStoGCS_end, boundingPath);
            }

            parameters.boundingRectangle = boundingPath.boundingRect();
            parameters.boundingRectangle.adjust(-lineEndingSize, -lineEndingSize, lineEndingSize, lineEndingSize);
            break;
        }

        default:
            Q_ASSERT(false);
            break;
    }
}

QColor PDFPolygonalGeometryAnnotation::getFillColor() const
{
    return getDrawColorFromAnnotationColor(getInteriorColor(), getFillOpacity());
}

PDFAnnotation::LineGeometryInfo PDFAnnotation::LineGeometryInfo::create(QLineF line)
{
    LineGeometryInfo result;
    result.originalLine = line;
    result.transformedLine = QLineF(QPointF(0, 0), QPointF(line.length(), 0));

    // Strategy: for simplification, we rotate the line clockwise so we will
    // get the line axis equal to the x-axis.
    const double angle = line.angleTo(QLineF(0, 0, 1, 0));

    QPointF p1 = line.p1();

    // Matrix LCStoGCS is local coordinate system of line line. It transforms
    // points on the line to the global coordinate system. So, point (0, 0) will
    // map onto p1 and point (length(p1-p2), 0) will map onto p2.
    result.LCStoGCS = QTransform();
    result.LCStoGCS.translate(p1.x(), p1.y());
    result.LCStoGCS.rotate(angle);
    result.GCStoLCS = result.LCStoGCS.inverted();

    return result;
}

void PDFAnnotation::drawLineEnding(QPainter* painter,
                                   QPointF point,
                                   PDFReal lineEndingSize,
                                   PDFReal arrowAxisLength,
                                   AnnotationLineEnding ending,
                                   bool flipAxis,
                                   const QTransform& LCStoGCS,
                                   QPainterPath& boundingPath) const
{
    QPainterPath path;
    const PDFReal lineEndingHalfSize = lineEndingSize * 0.5;

    switch (ending)
    {
        case AnnotationLineEnding::None:
            break;

        case AnnotationLineEnding::Square:
        {
            path.addRect(-lineEndingHalfSize, -lineEndingHalfSize, lineEndingSize, lineEndingSize);
            break;
        }

        case AnnotationLineEnding::Circle:
        {
            path.addEllipse(QPointF(0, 0), lineEndingHalfSize, lineEndingHalfSize);
            break;
        }

        case AnnotationLineEnding::Diamond:
        {
            path.moveTo(0.0, -lineEndingHalfSize);
            path.lineTo(lineEndingHalfSize, 0.0);
            path.lineTo(0.0, +lineEndingHalfSize);
            path.lineTo(-lineEndingHalfSize, 0.0);
            path.closeSubpath();
            break;
        }

        case AnnotationLineEnding::OpenArrow:
        {
            path.moveTo(0.0, 0.0);
            path.lineTo(arrowAxisLength, lineEndingHalfSize);
            path.moveTo(0.0, 0.0);
            path.lineTo(arrowAxisLength, -lineEndingHalfSize);
            break;
        }

        case AnnotationLineEnding::ClosedArrow:
        {
            path.moveTo(0.0, 0.0);
            path.lineTo(arrowAxisLength, lineEndingHalfSize);
            path.lineTo(arrowAxisLength, -lineEndingHalfSize);
            path.closeSubpath();
            break;
        }

        case AnnotationLineEnding::Butt:
        {
            path.moveTo(0.0, -lineEndingHalfSize);
            path.lineTo(0.0, lineEndingHalfSize);
            break;
        }

        case AnnotationLineEnding::ROpenArrow:
        {
            path.moveTo(0.0, 0.0);
            path.lineTo(-arrowAxisLength, lineEndingHalfSize);
            path.moveTo(0.0, 0.0);
            path.lineTo(-arrowAxisLength, -lineEndingHalfSize);
            break;
        }

        case AnnotationLineEnding::RClosedArrow:
        {
            path.moveTo(0.0, 0.0);
            path.lineTo(-arrowAxisLength, lineEndingHalfSize);
            path.lineTo(-arrowAxisLength, -lineEndingHalfSize);
            path.closeSubpath();
            break;
        }

        case AnnotationLineEnding::Slash:
        {
            const PDFReal angle = 60;
            const PDFReal lineEndingHalfSizeForSlash = lineEndingSize * 0.5;
            const PDFReal slashAxisLength = lineEndingHalfSizeForSlash / qTan(qDegreesToRadians(angle));

            path.moveTo(-slashAxisLength, -lineEndingHalfSizeForSlash);
            path.lineTo(slashAxisLength, lineEndingHalfSizeForSlash);
            break;
        }

        default:
            break;
    }

    if (!path.isEmpty())
    {
        // Flip the x-axis (we are drawing endpoint)
        if (flipAxis && ending != AnnotationLineEnding::Slash)
        {
            QTransform matrix;
            matrix.scale(-1.0, 1.0);
            path = matrix.map(path);
        }

        path.translate(point);
        path = LCStoGCS.map(path);
        painter->drawPath(path);
        boundingPath.addPath(path);
    }
}

void PDFAnnotation::drawLine(const PDFAnnotation::LineGeometryInfo& info,
                             QPainter& painter,
                             PDFReal lineEndingSize,
                             AnnotationLineEnding p1Ending,
                             AnnotationLineEnding p2Ending,
                             QPainterPath& boundingPath,
                             QPointF textOffset,
                             const QString& text,
                             bool textIsAboveLine) const
{
    const PDFReal angle = 30;
    const PDFReal lineEndingHalfSize = lineEndingSize * 0.5;
    const PDFReal arrowAxisLength = lineEndingHalfSize / qTan(qDegreesToRadians(angle));

    auto getOffsetFromLineEnding = [lineEndingHalfSize, arrowAxisLength](AnnotationLineEnding ending)
    {
        switch (ending)
        {
            case AnnotationLineEnding::Square:
            case AnnotationLineEnding::Circle:
            case AnnotationLineEnding::Diamond:
                return lineEndingHalfSize;
            case AnnotationLineEnding::ClosedArrow:
                return arrowAxisLength;

            default:
                break;
        }

        return 0.0;
    };

    // Remove the offset from start/end
    const PDFReal startOffset = getOffsetFromLineEnding(p1Ending);
    const PDFReal endOffset = getOffsetFromLineEnding(p2Ending);

    QLineF adjustedLine = info.transformedLine;
    adjustedLine.setP1(adjustedLine.p1() + QPointF(startOffset, 0));
    adjustedLine.setP2(adjustedLine.p2() - QPointF(endOffset, 0));

    int textVerticalOffset = 0;
    const bool drawText = !text.isEmpty();
    QPainterPath textPath;

    if (drawText)
    {
        QFont font = painter.font();
        font.setPixelSize(12.0);

        QFontMetricsF fontMetrics(font, painter.device());
        textVerticalOffset = fontMetrics.descent() + fontMetrics.leading();

        textPath.addText(0, 0, font, text);
        textPath = QTransform(1, 0, 0, -1, 0, 0).map(textPath);
    }

    drawLineEnding(&painter, info.transformedLine.p1(), lineEndingSize, arrowAxisLength, p1Ending, false, info.LCStoGCS, boundingPath);
    drawLineEnding(&painter, info.transformedLine.p2(), lineEndingSize, arrowAxisLength, p2Ending, true, info.LCStoGCS, boundingPath);

    if (drawText && !textIsAboveLine)
    {
        // We will draw text in the line
        QRectF textBoundingRect = textPath.controlPointRect();
        QPointF center = textBoundingRect.center();
        const qreal offsetY = center.y();
        const qreal offsetX = center.x();
        textPath.translate(textOffset + adjustedLine.center() - QPointF(offsetX, offsetY));
        textBoundingRect = textPath.controlPointRect();

        const qreal textPadding = 3.0;
        const qreal textStart = textBoundingRect.left() - textPadding;
        const qreal textEnd = textBoundingRect.right() + textPadding;

        textPath = info.LCStoGCS.map(textPath);
        painter.fillPath(textPath, QBrush(painter.pen().color(), Qt::SolidPattern));
        boundingPath.addPath(textPath);

        if (textStart > adjustedLine.p1().x())
        {
            QLineF leftLine(adjustedLine.p1(), QPointF(textStart, adjustedLine.p2().y()));
            painter.drawLine(info.LCStoGCS.map(leftLine));
        }

        if (textEnd < adjustedLine.p2().x())
        {
            QLineF rightLine(QPointF(textEnd, adjustedLine.p2().y()), adjustedLine.p2());
            painter.drawLine(info.LCStoGCS.map(rightLine));
        }

        // Include whole line to the bounding path
        adjustedLine = info.LCStoGCS.map(adjustedLine);
        boundingPath.moveTo(adjustedLine.p1());
        boundingPath.lineTo(adjustedLine.p2());
    }
    else
    {
        if (drawText)
        {
            // We will draw text above the line
            QRectF textBoundingRect = textPath.controlPointRect();
            const qreal offsetY = textBoundingRect.top() - textVerticalOffset;
            const qreal offsetX = textBoundingRect.center().x();
            textPath.translate(textOffset + adjustedLine.center() - QPointF(offsetX, offsetY));

            textPath = info.LCStoGCS.map(textPath);
            painter.fillPath(textPath, QBrush(painter.pen().color(), Qt::SolidPattern));
            boundingPath.addPath(textPath);
        }

        adjustedLine = info.LCStoGCS.map(adjustedLine);
        painter.drawLine(adjustedLine);

        boundingPath.moveTo(adjustedLine.p1());
        boundingPath.lineTo(adjustedLine.p2());
    }
}

void PDFHighlightAnnotation::draw(AnnotationDrawParameters& parameters) const
{
    if (m_highlightArea.isEmpty())
    {
        // Jakub Melka: do not draw empty highlight area
        return;
    }

    QPainter& painter = *parameters.painter;
    painter.setCompositionMode(getCompositionMode());
    parameters.boundingRectangle = m_highlightArea.getPath().boundingRect();

    painter.setPen(getPen());
    painter.setBrush(getBrush());
    switch (m_type)
    {
        case AnnotationType::Highlight:
        {
            painter.setCompositionMode(QPainter::CompositionMode_Multiply);
            painter.fillPath(m_highlightArea.getPath(), QBrush(getStrokeColor(), Qt::SolidPattern));
            break;
        }

        case AnnotationType::Underline:
        {
            for (const PDFAnnotationQuadrilaterals::Quadrilateral& quadrilateral : m_highlightArea.getQuadrilaterals())
            {
                QPointF p1 = quadrilateral[0];
                QPointF p2 = quadrilateral[1];
                QLineF line(p1, p2);
                painter.drawLine(line);
            }
            break;
        }

        case AnnotationType::Squiggly:
        {
            // Jakub Melka: Squiggly underline
            for (const PDFAnnotationQuadrilaterals::Quadrilateral& quadrilateral : m_highlightArea.getQuadrilaterals())
            {
                QPointF p1 = quadrilateral[0];
                QPointF p2 = quadrilateral[1];

                // Calculate length (height) of quadrilateral
                const PDFReal height = (QLineF(quadrilateral[0], quadrilateral[2]).length() + QLineF(quadrilateral[1], quadrilateral[3]).length()) * 0.5;
                const PDFReal markSize = height / 7.0;

                // We can't assume, that line is horizontal. For example, rotated text with 45° degrees
                // counterclockwise, if it is highlighted with squiggly underline, it is not horizontal line.
                // So, we must calculate line geometry and transform it.
                QLineF line(p1, p2);
                LineGeometryInfo lineGeometryInfo = LineGeometryInfo::create(line);

                bool leadingEdge = true;
                for (PDFReal x = lineGeometryInfo.transformedLine.p1().x(); x < lineGeometryInfo.transformedLine.p2().x(); x+= markSize)
                {
                    QLineF edgeLine;
                    if (leadingEdge)
                    {
                        edgeLine = QLineF(x, 0.0, x + markSize, markSize);
                    }
                    else
                    {
                        // Falling edge
                        edgeLine = QLineF(x, markSize, x + markSize, 0.0);
                    }

                    QLineF transformedLine = lineGeometryInfo.LCStoGCS.map(edgeLine);
                    painter.drawLine(transformedLine);
                    leadingEdge = !leadingEdge;
                }
            }
            break;
        }

        case AnnotationType::StrikeOut:
        {
            for (const PDFAnnotationQuadrilaterals::Quadrilateral& quadrilateral : m_highlightArea.getQuadrilaterals())
            {
                QPointF p1 = (quadrilateral[0] + quadrilateral[2]) * 0.5;
                QPointF p2 = (quadrilateral[1] + quadrilateral[3]) * 0.5;
                QLineF line(p1, p2);
                painter.drawLine(line);
            }
            break;
        }

        default:
            break;
    }

    const qreal penWidth = painter.pen().widthF();
    parameters.boundingRectangle.adjust(-penWidth, -penWidth, penWidth, penWidth);
}

std::vector<PDFAppeareanceStreams::Key> PDFLinkAnnotation::getDrawKeys(const PDFFormManager* formManager) const
{
    Q_UNUSED(formManager);

    return { PDFAppeareanceStreams::Key{ PDFAppeareanceStreams::Appearance::Down, QByteArray() } };
}

void PDFLinkAnnotation::draw(AnnotationDrawParameters& parameters) const
{
    if (parameters.key.first != PDFAppeareanceStreams::Appearance::Down ||
        m_activationRegion.isEmpty() ||
        m_highlightMode == LinkHighlightMode::None)
    {
        // Nothing to draw
        return;
    }

    QPainter& painter = *parameters.painter;
    parameters.boundingRectangle = getRectangle();

    switch (m_highlightMode)
    {
        case LinkHighlightMode::Invert:
        {
            // Invert all
            painter.setCompositionMode(QPainter::CompositionMode_Difference);
            painter.fillPath(m_activationRegion.getPath(), QBrush(Qt::white, Qt::SolidPattern));
            break;
        }

        case LinkHighlightMode::Outline:
        {
            // Invert the border
            painter.setCompositionMode(QPainter::CompositionMode_Difference);
            QPen pen = getPen();
            pen.setColor(Qt::white);
            painter.setPen(pen);
            painter.setBrush(Qt::NoBrush);
            painter.drawPath(m_activationRegion.getPath());
            break;
        }

        case LinkHighlightMode::Push:
        {
            // Draw border
            painter.setCompositionMode(getCompositionMode());
            painter.setPen(getPen());
            painter.setBrush(Qt::NoBrush);
            painter.drawPath(m_activationRegion.getPath());
            break;
        }

        default:
            Q_ASSERT(false);
            break;
    }
}

PDFAnnotationDefaultAppearance PDFAnnotationDefaultAppearance::parse(const QByteArray& string)
{
    PDFAnnotationDefaultAppearance result;
    PDFLexicalAnalyzer analyzer(string.constData(), string.constData() + string.size());

    std::vector<PDFLexicalAnalyzer::Token> tokens;

    for (PDFLexicalAnalyzer::Token token = analyzer.fetch(); token.type != PDFLexicalAnalyzer::TokenType::EndOfFile; token = analyzer.fetch())
    {
        tokens.push_back(qMove(token));
    }

    auto readNumber = [&tokens](size_t index) -> PDFReal
    {
        Q_ASSERT(index < tokens.size());
        const PDFLexicalAnalyzer::Token& token = tokens[index];
        if (token.type == PDFLexicalAnalyzer::TokenType::Real ||
            token.type == PDFLexicalAnalyzer::TokenType::Integer)
        {
            return token.data.toDouble();
        }

        return 0.0;
    };

    for (size_t i = 0; i < tokens.size(); ++i)
    {
        const PDFLexicalAnalyzer::Token& token = tokens[i];
        if (token.type == PDFLexicalAnalyzer::TokenType::Command)
        {
            QByteArray command = token.data.toByteArray();
            if (command == "Tf")
            {
                if (i >= 1)
                {
                    result.m_fontSize = readNumber(i - 1);
                }
                if (i >= 2)
                {
                    result.m_fontName = tokens[i - 2].data.toByteArray();
                }
            }
            else if (command == "g" && i >= 1)
            {
                result.m_fontColor = PDFAnnotation::getDrawColorFromAnnotationColor({ readNumber(i - 1) }, 1.0);
            }
            else if (command == "rg" && i >= 3)
            {
                result.m_fontColor = PDFAnnotation::getDrawColorFromAnnotationColor({ readNumber(i - 3), readNumber(i - 2), readNumber(i - 1) }, 1.0);
            }
            else if (command == "k" && i >= 4)
            {
                result.m_fontColor = PDFAnnotation::getDrawColorFromAnnotationColor({ readNumber(i - 4), readNumber(i - 3), readNumber(i - 2), readNumber(i - 1) }, 1.0);
            }
        }
    }

    return result;
}

void PDFFreeTextAnnotation::draw(AnnotationDrawParameters& parameters) const
{
    QPainter& painter = *parameters.painter;
    painter.setCompositionMode(getCompositionMode());
    parameters.boundingRectangle = getRectangle();

    painter.setPen(getPen());
    painter.setBrush(getBrush());

    // Draw callout line
    const PDFAnnotationCalloutLine& calloutLine = getCalloutLine();

    switch (calloutLine.getType())
    {
        case PDFAnnotationCalloutLine::Type::Invalid:
        {
            // Annotation doesn't have callout line
            break;
        }

        case PDFAnnotationCalloutLine::Type::StartEnd:
        {
            QPainterPath boundingPath;
            QLineF line(calloutLine.getPoint(0), calloutLine.getPoint(1));
            const PDFReal lineEndingSize = qMin(painter.pen().widthF() * 5.0, line.length() * 0.5);
            drawLine(LineGeometryInfo::create(line), painter, lineEndingSize,
                     getStartLineEnding(), getEndLineEnding(), boundingPath,
                     QPointF(), QString(), true);
            break;
        }

        case PDFAnnotationCalloutLine::Type::StartKneeEnd:
        {
            QPainterPath boundingPath;

            QLineF lineStart(calloutLine.getPoint(0), calloutLine.getPoint(1));
            QLineF lineEnd(calloutLine.getPoint(1), calloutLine.getPoint(2));

            PDFReal preferredLineEndingSize = painter.pen().widthF() * 5.0;
            PDFReal lineStartEndingSize = qMin(preferredLineEndingSize, lineStart.length() * 0.5);
            drawLine(LineGeometryInfo::create(lineStart), painter, lineStartEndingSize,
                     getStartLineEnding(), AnnotationLineEnding::None, boundingPath,
                     QPointF(), QString(), true);


            PDFReal lineEndEndingSize = qMin(preferredLineEndingSize, lineEnd.length() * 0.5);
            drawLine(LineGeometryInfo::create(lineEnd), painter, lineEndEndingSize,
                     AnnotationLineEnding::None, getEndLineEnding() , boundingPath,
                     QPointF(), QString(), true);
            break;
        }

        default:
        {
            Q_ASSERT(false);
            break;
        }
    }

    QRectF textRect = getTextRectangle();
    if (!textRect.isValid())
    {
        textRect = getRectangle();
    }

    painter.drawRect(textRect);

    // Draw text
    PDFAnnotationDefaultAppearance defaultAppearance = PDFAnnotationDefaultAppearance::parse(getDefaultAppearance());

    QFont font(defaultAppearance.getFontName());
    font.setPixelSize(defaultAppearance.getFontSize());
    painter.setPen(defaultAppearance.getFontColor());

    Qt::Alignment alignment = Qt::AlignTop;
    switch (getJustification())
    {
        case PDFFreeTextAnnotation::Justification::Left:
            alignment |= Qt::AlignLeft;
            break;

        case PDFFreeTextAnnotation::Justification::Centered:
            alignment |= Qt::AlignHCenter;
            break;

        case PDFFreeTextAnnotation::Justification::Right:
            alignment |= Qt::AlignRight;
            break;

        default:
            alignment |= Qt::AlignLeft;
            Q_ASSERT(false);
            break;
    }

    QTextOption option(alignment);
    option.setWrapMode(QTextOption::WrapAtWordBoundaryOrAnywhere);
    option.setUseDesignMetrics(true);

    painter.translate(textRect.left(), textRect.bottom());
    painter.scale(1.0, -1.0);
    painter.drawText(QRectF(QPointF(0, 0), textRect.size()), getContents(), option);
}

void PDFCaretAnnotation::draw(AnnotationDrawParameters& parameters) const
{
    QPainter& painter = *parameters.painter;
    painter.setCompositionMode(getCompositionMode());
    parameters.boundingRectangle = getRectangle();

    QRectF caretRect = getCaretRectangle();
    if (caretRect.isEmpty())
    {
        caretRect = getRectangle();
    }

    QPointF controlPoint(caretRect.center());
    controlPoint.setY(caretRect.top());

    QPointF topPoint = controlPoint;
    topPoint.setY(caretRect.bottom());

    QPainterPath path;
    path.moveTo(caretRect.topLeft());
    path.quadTo(controlPoint, topPoint);
    path.quadTo(controlPoint, caretRect.topRight());
    path.lineTo(caretRect.topLeft());
    path.closeSubpath();

    painter.fillPath(path, QBrush(getStrokeColor(), Qt::SolidPattern));
}

void PDFInkAnnotation::draw(AnnotationDrawParameters& parameters) const
{
    QPainter& painter = *parameters.painter;
    QPainterPath path = getInkPath();

    painter.setPen(getPen());
    painter.setBrush(getBrush());
    painter.setCompositionMode(getCompositionMode());

    QPainterPath boundingPath;
    QPainterPath currentPath;
    const int elementCount = path.elementCount();
    bool hasSpline = false;
    for (int i = 0; i < elementCount; ++i)
    {
        QPainterPath::Element element = path.elementAt(i);
        switch (element.type)
        {
            case QPainterPath::MoveToElement:
            {
                // Reset the path
                if (!currentPath.isEmpty())
                {
                    boundingPath.addPath(currentPath);
                    painter.drawPath(currentPath);
                    currentPath.clear();
                }
                currentPath.moveTo(element.x, element.y);
                break;
            }

            case QPainterPath::LineToElement:
            {
                const QPointF p0 = currentPath.currentPosition();
                const QPointF p1(element.x, element.y);

                QLineF line(p0, p1);
                QPointF normal = line.normalVector().p2() - p0;

                // Jakub Melka: This computation should be clarified. We use second order Bezier curves.
                // We must calculate single control point. Let B(t) is bezier curve of second order.
                // Then second derivation is B''(t) = 2(p0 - 2*Pc + p1). Second derivation curve is its
                // normal. So, we calculate normal vector of the line (which has norm equal to line length),
                // then we get following formula:
                //
                // Pc = (p0 + p1) / 2 - B''(t) / 4
                //
                // So, for bezier curves of second order, second derivative is constant (which is not surprising,
                // because second derivative of polynomial of order 2 is also a constant). Control point is then
                // used to paint the path.
                QPointF controlPoint = -normal * 0.25 + (p0 + p1) * 0.5;
                currentPath.quadTo(controlPoint, p1);
                break;
            }

            case QPainterPath::CurveToElement:
            case QPainterPath::CurveToDataElement:
                hasSpline = true;
                break;

            default:
                Q_ASSERT(false);
                break;
        }

        // Jakub Melka: If we have a spline, then we don't do anything...
        // Just copy the spline path.
        if (hasSpline)
        {
            currentPath = path;
            break;
        }
    }

    // Reset the path
    if (!currentPath.isEmpty())
    {
        boundingPath.addPath(currentPath);
        painter.drawPath(currentPath);
        currentPath.clear();
    }

    const qreal penWidth = painter.pen().widthF();
    parameters.boundingRectangle = boundingPath.boundingRect();
    parameters.boundingRectangle.adjust(-penWidth, -penWidth, penWidth, penWidth);
}

void PDFStampAnnotation::draw(AnnotationDrawParameters& parameters) const
{
    QPainter& painter = *parameters.painter;
    painter.setCompositionMode(getCompositionMode());

    QString text = getText(m_stamp, false);
    QColor color(Qt::red);

    switch (m_stamp)
    {
        case Stamp::Approved:
            color = Qt::green;
            break;

        case Stamp::AsIs:
        case Stamp::Confidential:
            break;

        case Stamp::Departmental:
            color = Qt::blue;
            break;

        case Stamp::Draft:
            break;

        case Stamp::Experimental:
            color = Qt::blue;
            break;

        case Stamp::Expired:
        case Stamp::Final:
            break;

        case Stamp::ForComment:
            color = Qt::green;
            break;

        case Stamp::ForPublicRelease:
            color = Qt::green;
            break;

        case Stamp::NotApproved:
        case Stamp::NotForPublicRelease:
            break;

        case Stamp::Sold:
            color = Qt::blue;
            break;

        case Stamp::TopSecret:
            break;

        default:
            Q_ASSERT(false);
            break;
    }

    color.setAlphaF(getFillOpacity());

    const PDFReal textHeight = 16;
    QFont font("Courier New");
    font.setBold(true);
    font.setPixelSize(textHeight);

    QFontMetricsF fontMetrics(font, painter.device());
    const qreal textWidth = fontMetrics.horizontalAdvance(text);
    const qreal rectangleWidth = textWidth + 10;
    const qreal rectangleHeight = textHeight * 1.2;
    const qreal penWidth = 2.0;

    QRectF rectangle = getRectangle();
    rectangle.setSize(QSizeF(rectangleWidth, rectangleHeight));

    QPen pen(color);
    pen.setWidthF(penWidth);
    painter.setPen(pen);
    painter.setBrush(Qt::NoBrush);

    painter.drawRoundedRect(rectangle, 5, 5, Qt::AbsoluteSize);

    // Draw text
    QPainterPath textPath;
    textPath.addText(0, 0, font, text);
    textPath = QTransform(1, 0, 0, -1, 0, 0).map(textPath);

    QPointF center = textPath.boundingRect().center();
    textPath.translate(rectangle.center() - center);
    painter.fillPath(textPath, QBrush(color, Qt::SolidPattern));

    parameters.boundingRectangle = rectangle;
    parameters.boundingRectangle.adjust(-penWidth, -penWidth, penWidth, penWidth);
}

QString PDFStampAnnotation::getText(Stamp stamp, bool isActionText)
{
    QString text;

    switch (stamp)
    {
        case Stamp::Approved:
            text = isActionText ? PDFTranslationContext::tr("&Approved")
                                : PDFTranslationContext::tr("APPROVED");
            break;

        case Stamp::AsIs:
            text = isActionText ? PDFTranslationContext::tr("As &Is")
                                : PDFTranslationContext::tr("AS IS");
            break;

        case Stamp::Confidential:
            text = isActionText ? PDFTranslationContext::tr("&Confidential")
                                : PDFTranslationContext::tr("CONFIDENTIAL");
            break;

        case Stamp::Departmental:
            text = isActionText ? PDFTranslationContext::tr("&Departmental")
                                : PDFTranslationContext::tr("DEPARTMENTAL");
            break;

        case Stamp::Draft:
            text = isActionText ? PDFTranslationContext::tr("Dra&ft")
                                : PDFTranslationContext::tr("DRAFT");
            break;

        case Stamp::Experimental:
            text = isActionText ? PDFTranslationContext::tr("&Experimental")
                                : PDFTranslationContext::tr("EXPERIMENTAL");
            break;

        case Stamp::Expired:
            text = isActionText ? PDFTranslationContext::tr("E&xpired")
                                : PDFTranslationContext::tr("EXPIRED");
            break;

        case Stamp::Final:
            text = isActionText ? PDFTranslationContext::tr("Fina&l")
                                : PDFTranslationContext::tr("FINAL");
            break;

        case Stamp::ForComment:
            text = isActionText ? PDFTranslationContext::tr("For Co&mment")
                                : PDFTranslationContext::tr("FOR COMMENT");
            break;

        case Stamp::ForPublicRelease:
            text = isActionText ? PDFTranslationContext::tr("For P&ublic Release")
                                : PDFTranslationContext::tr("FOR PUBLIC RELEASE");
            break;

        case Stamp::NotApproved:
            text = isActionText ? PDFTranslationContext::tr("Not A&pproved")
                                : PDFTranslationContext::tr("NOT APPROVED");
            break;

        case Stamp::NotForPublicRelease:
            text = isActionText ? PDFTranslationContext::tr("N&ot For Public Release")
                                : PDFTranslationContext::tr("NOT FOR PUBLIC RELEASE");
            break;

        case Stamp::Sold:
            text = isActionText ? PDFTranslationContext::tr("&Sold")
                                : PDFTranslationContext::tr("SOLD");
            break;

        case Stamp::TopSecret:
            text = isActionText ? PDFTranslationContext::tr("&Top Secret")
                                : PDFTranslationContext::tr("TOP SECRET");
            break;

        default:
            Q_ASSERT(false);
            break;
    }

    return text;
}

void PDFStampAnnotation::setStamp(const Stamp& stamp)
{
    m_stamp = stamp;
}

void PDFStampAnnotation::setIntent(const StampIntent& intent)
{
    m_intent = intent;
}

void PDFAnnotation::drawCharacterSymbol(QString text, PDFReal opacity, AnnotationDrawParameters& parameters) const
{
    QColor strokeColor = QColor::fromRgbF(0.0, 0.0, 0.0, opacity);

    constexpr const PDFReal rectSize = 24.0;
    QPainter& painter = *parameters.painter;
    QRectF rectangle = getRectangle();
    rectangle.setSize(QSizeF(rectSize, rectSize));

    QFont font = painter.font();
    font.setPixelSize(16.0);

    QPainterPath textPath;
    textPath.addText(0.0, 0.0, font, text);
    textPath = QTransform(1.0, 0.0, 0.0, -1.0, 0.0, 0.0).map(textPath);
    QRectF textBoundingRect = textPath.boundingRect();
    QPointF offset = rectangle.center() - textBoundingRect.center();
    textPath.translate(offset);
    painter.fillPath(textPath, QBrush(strokeColor, Qt::SolidPattern));

    parameters.boundingRectangle = rectangle;
}

void PDFFileAttachmentAnnotation::draw(AnnotationDrawParameters& parameters) const
{
    QString text = "?";
    switch (getIcon())
    {
        case FileAttachmentIcon::Graph:
            text = QString::fromUtf16(u"\U0001F4C8");
            break;
        case FileAttachmentIcon::Paperclip:
            text = QString::fromUtf16(u"\U0001F4CE");
            break;
        case FileAttachmentIcon::PushPin:
            text = QString::fromUtf16(u"\U0001F4CC");
            break;
        case FileAttachmentIcon::Tag:
            text = QString::fromUtf16(u"\U0001F3F7");
            break;

        default:
            Q_ASSERT(false);
            break;
    }

    parameters.painter->setCompositionMode(getCompositionMode());
    drawCharacterSymbol(text, getStrokeOpacity(), parameters);
}

void PDFSoundAnnotation::draw(AnnotationDrawParameters& parameters) const
{
    QString text = "?";
    switch (getIcon())
    {
        case Icon::Speaker:
            text = QString::fromUtf16(u"\U0001F508");
            break;
        case Icon::Microphone:
            text = QString::fromUtf16(u"\U0001F3A4");
            break;

        default:
            Q_ASSERT(false);
            break;
    }

    parameters.painter->setCompositionMode(getCompositionMode());
    drawCharacterSymbol(text, getStrokeOpacity(), parameters);
}

const PDFAnnotationManager::PageAnnotation* PDFAnnotationManager::PageAnnotations::getPopupAnnotation(const PageAnnotation& pageAnnotation) const
{
    const PDFMarkupAnnotation* markupAnnotation = pageAnnotation.annotation->asMarkupAnnotation();
    if (markupAnnotation)
    {
        const PDFObjectReference popupAnnotation = markupAnnotation->getPopupAnnotation();
        auto it = std::find_if(annotations.cbegin(), annotations.cend(), [popupAnnotation](const PageAnnotation& pa) { return pa.annotation->getSelfReference() == popupAnnotation; });
        if (it != annotations.cend())
        {
            return &*it;
        }
    }

    return nullptr;
}

std::vector<const PDFAnnotationManager::PageAnnotation*> PDFAnnotationManager::PageAnnotations::getReplies(const PageAnnotation& pageAnnotation) const
{
    std::vector<const PageAnnotation*> result;

    const PDFObjectReference reference = pageAnnotation.annotation->getSelfReference();
    for (size_t i = 0, count = annotations.size(); i < count; ++i)
    {
        const PageAnnotation& currentAnnotation = annotations[i];
        if (currentAnnotation.annotation->isReplyTo())
        {
            const PDFMarkupAnnotation* markupAnnotation = currentAnnotation.annotation->asMarkupAnnotation();
            Q_ASSERT(markupAnnotation);

            if (markupAnnotation->getInReplyTo() == reference)
            {
                result.push_back(&currentAnnotation);
            }
        }
    }

    auto comparator = [](const PageAnnotation* l, const PageAnnotation* r)
    {
        QDateTime leftDateTime = l->annotation->getLastModifiedDateTime();
        QDateTime rightDateTime = r->annotation->getLastModifiedDateTime();

        if (const PDFMarkupAnnotation* markupL = l->annotation->asMarkupAnnotation())
        {
            leftDateTime = markupL->getCreationDate();
        }

        if (const PDFMarkupAnnotation* markupR = r->annotation->asMarkupAnnotation())
        {
            rightDateTime = markupR->getCreationDate();
        }

        return leftDateTime < rightDateTime;
    };
    std::sort(result.begin(), result.end(), comparator);

    return result;
}

void PDFWidgetAnnotation::draw(AnnotationDrawParameters& parameters) const
{
    // Do not draw without form manager
    if (!parameters.formManager)
    {
        return;
    }

    // Do not draw without form field
    const PDFFormField* formField = parameters.formManager->getFormFieldForWidget(getSelfReference());
    if (!formField)
    {
        return;
    }

    PDFPainterStateGuard guard(parameters.painter);
    parameters.painter->setCompositionMode(getCompositionMode());

    if (parameters.formManager->isEditorDrawEnabled(formField))
    {
        parameters.formManager->drawFormField(formField, parameters, true);
    }
    else
    {
        switch (formField->getFieldType())
        {
            case PDFFormField::FieldType::Text:
            case PDFFormField::FieldType::Choice:
            {
                parameters.formManager->drawFormField(formField, parameters, false);
                break;
            }

            case PDFFormField::FieldType::Button:
            {
                const PDFFormFieldButton* button = dynamic_cast<const PDFFormFieldButton*>(formField);
                switch (button->getButtonType())
                {
                    case PDFFormFieldButton::ButtonType::PushButton:
                    {
                        QRectF rectangle = getRectangle();

                        if (!getContents().isEmpty())
                        {
                            QByteArray defaultAppearance = parameters.formManager->getForm()->getDefaultAppearance().value_or(QByteArray());
                            PDFAnnotationDefaultAppearance appearance = PDFAnnotationDefaultAppearance::parse(defaultAppearance);

                            qreal fontSize = appearance.getFontSize();
                            if (qFuzzyIsNull(fontSize))
                            {
                                fontSize = rectangle.height() * 0.6;
                            }

                            QFont font(appearance.getFontName());
                            font.setHintingPreference(QFont::PreferNoHinting);
                            font.setPixelSize(qCeil(fontSize));
                            font.setStyleStrategy(QFont::ForceOutline);

                            QPainter* painter = parameters.painter;
                            painter->translate(rectangle.bottomLeft());
                            painter->scale(1.0, -1.0);
                            painter->setFont(font);

                            // Draw border
                            QRectF drawRect(0, 0, rectangle.width(), rectangle.height());
                            painter->setPen(getPen());
                            painter->setBrush(QBrush(Qt::lightGray));
                            painter->drawRect(drawRect);
                            painter->drawText(drawRect, Qt::AlignCenter, getContents());
                        }
                        else
                        {
                            // This is push button without text. Just mark the area as highlighted.
                            QPainter* painter = parameters.painter;

                            if (parameters.key.first == PDFAppeareanceStreams::Appearance::Rollover ||
                                parameters.key.first == PDFAppeareanceStreams::Appearance::Down)
                            {
                                switch (m_highlightMode)
                                {
                                    case HighlightMode::Invert:
                                    {
                                        // Invert all
                                        painter->setCompositionMode(QPainter::CompositionMode_Difference);
                                        painter->fillRect(rectangle, QBrush(Qt::white, Qt::SolidPattern));
                                        break;
                                    }

                                    case HighlightMode::Outline:
                                    {
                                        // Invert the border
                                        painter->setCompositionMode(QPainter::CompositionMode_Difference);
                                        QPen pen = getPen();
                                        pen.setColor(Qt::white);
                                        painter->setPen(pen);
                                        painter->setBrush(Qt::NoBrush);
                                        painter->drawRect(rectangle);
                                        break;
                                    }

                                    case HighlightMode::Push:
                                    {
                                        // Draw border
                                        painter->setCompositionMode(getCompositionMode());
                                        painter->setPen(getPen());
                                        painter->setBrush(Qt::NoBrush);
                                        painter->drawRect(rectangle);
                                        break;
                                    }

                                    default:
                                        Q_ASSERT(false);
                                        break;
                                }
                            }
                        }
                        break;
                    }

                    case PDFFormFieldButton::ButtonType::RadioButton:
                    {
                        QRectF rectangle = getRectangle();
                        QPainter* painter = parameters.painter;

                        rectangle.setWidth(rectangle.height());

                        painter->setPen(Qt::black);
                        painter->setBrush(Qt::NoBrush);
                        painter->drawEllipse(rectangle);

                        if (parameters.key.second != "Off")
                        {
                            QRectF rectangleMark = rectangle;
                            rectangleMark.setWidth(rectangle.width() * 0.75);
                            rectangleMark.setHeight(rectangle.height() * 0.75);
                            rectangleMark.moveCenter(rectangle.center());

                            painter->setPen(Qt::NoPen);
                            painter->setBrush(QBrush(Qt::black));

                            painter->drawEllipse(rectangleMark);
                        }
                        break;
                    }

                    case PDFFormFieldButton::ButtonType::CheckBox:
                    {
                        QRectF rectangle = getRectangle();
                        QPainter* painter = parameters.painter;

                        rectangle.setWidth(rectangle.height());

                        painter->setPen(Qt::black);
                        painter->setBrush(Qt::NoBrush);
                        painter->drawRect(rectangle);

                        if (parameters.key.second != "Off")
                        {
                            QRectF rectangleMark = rectangle;
                            rectangleMark.setWidth(rectangle.width() * 0.75);
                            rectangleMark.setHeight(rectangle.height() * 0.75);
                            rectangleMark.moveCenter(rectangle.center());

                            painter->drawLine(rectangleMark.topLeft(), rectangleMark.bottomRight());
                            painter->drawLine(rectangleMark.bottomLeft(), rectangleMark.topRight());
                        }
                        break;
                    }

                    default:
                    {
                        Q_ASSERT(false);
                        break;
                    }
                }

                break;
            }

            case PDFFormField::FieldType::Invalid:
            case PDFFormField::FieldType::Signature:
                break;

            default:
            {
                Q_ASSERT(false);
                break;
            }
        }
    }
}

std::vector<pdf::PDFAppeareanceStreams::Key> PDFWidgetAnnotation::getDrawKeys(const PDFFormManager* formManager) const
{
    if (!formManager)
    {
        return PDFAnnotation::getDrawKeys(formManager);
    }

    std::vector<pdf::PDFAppeareanceStreams::Key> result;

    // Try get the form field, if we find it, then determine from form field type
    // the list of appearance states.
    const PDFFormField* formField = formManager->getFormFieldForWidget(getSelfReference());
    if (!formField)
    {
        return PDFAnnotation::getDrawKeys(formManager);
    }

    switch (formField->getFieldType())
    {
        case PDFFormField::FieldType::Invalid:
            break;

        case PDFFormField::FieldType::Button:
        {
            const PDFFormFieldButton* button = dynamic_cast<const PDFFormFieldButton*>(formField);
            switch (button->getButtonType())
            {
                case PDFFormFieldButton::ButtonType::PushButton:
                {
                    result = { PDFAppeareanceStreams::Key{ PDFAppeareanceStreams::Appearance::Normal, QByteArray() },
                               PDFAppeareanceStreams::Key{ PDFAppeareanceStreams::Appearance::Rollover, QByteArray() },
                               PDFAppeareanceStreams::Key{ PDFAppeareanceStreams::Appearance::Down, QByteArray() } };
                    break;
                }

                case PDFFormFieldButton::ButtonType::RadioButton:
                case PDFFormFieldButton::ButtonType::CheckBox:
                {
                    result = getAppearanceStreams().getAppearanceKeys();
                    PDFAppeareanceStreams::Key offKey{ PDFAppeareanceStreams::Appearance::Normal, QByteArray() };
                    if (std::find(result.cbegin(), result.cend(), offKey) == result.cend())
                    {
                        result.push_back(qMove(offKey));
                    }
                    break;
                }

                default:
                {
                    Q_ASSERT(false);
                    break;
                }
            }

            break;
        }

        case PDFFormField::FieldType::Text:
            // Text has only default appearance
            break;

        case PDFFormField::FieldType::Choice:
            // Choices have always default appearance
            break;

        case PDFFormField::FieldType::Signature:
            // Signatures have always default appearance
            break;
    }

    if (result.empty())
    {
        result = PDFAnnotation::getDrawKeys(formManager);
    }

    return result;
}

void PDFRedactAnnotation::draw(AnnotationDrawParameters& parameters) const
{
    if (m_redactionRegion.isEmpty())
    {
        // Jakub Melka: do not draw empty redact area
        return;
    }

    QPainter& painter = *parameters.painter;
    painter.setCompositionMode(getCompositionMode());
    parameters.boundingRectangle = m_redactionRegion.getPath().boundingRect();

    painter.setPen(getPen());
    painter.setBrush(getBrush());
    painter.drawPath(m_redactionRegion.getPath());

    const qreal penWidth = painter.pen().widthF();
    parameters.boundingRectangle.adjust(-penWidth, -penWidth, penWidth, penWidth);
}

QColor PDFRedactAnnotation::getFillColor() const
{
    return getDrawColorFromAnnotationColor(getInteriorColor(), getFillOpacity());
}

}   // namespace pdf
