/*
    PosteRazor - Make your own poster!
    Copyright (C) 2005-2018 by Alessandro Portale
    http://posterazor.sourceforge.net/

    This file is part of PosteRazor

    PosteRazor is free software; you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    PosteRazor 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 General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with PosteRazor; if not, write to the Free Software
    Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/

#include "pdfwriter.h"
#include "posterazorcore.h"
#if defined (FREEIMAGE_LIB)
#    include "imageloaderfreeimage.h"
#else
#    include "imageloaderqt.h"
#endif

#include <QBrush>
#include <QFile>
#include <QSettings>
#include <QStringList>

#include <cmath>

const QLatin1String defaultValue_PaperFormat(           "DIN A4");

const QLatin1String settingsKey_PosterSizeMode(         "PosterSizeMode");
const QLatin1String settingsKey_PosterDimension(        "PosterDimension");
const QLatin1String settingsKey_PosterDimensionIsWidth( "PosterDimensionIsWidth");
const QLatin1String settingsKey_PosterAlignment(        "PosterAlignment");
const QLatin1String settingsKey_PaperFormat(            "PaperFormat");
const QLatin1String settingsKey_PaperOrientation(       "PaperOrientation");
const QLatin1String settingsKey_PaperBorderTop(         "PaperBorderTop");
const QLatin1String settingsKey_PaperBorderRight(       "PaperBorderRight");
const QLatin1String settingsKey_PaperBorderBottom(      "PaperBorderBottom");
const QLatin1String settingsKey_PaperBorderLeft(        "PaperBorderLeft");
const QLatin1String settingsKey_CustomPaperWidth(       "CustomPaperWidth");
const QLatin1String settingsKey_CustomPaperHeight(      "CustomPaperHeight");
const QLatin1String settingsKey_UseCustomPaperSize(     "UsesCustomPaperSize");
const QLatin1String settingsKey_OverlappingWidth(       "OverlappingWidth");
const QLatin1String settingsKey_OverlappingHeight(      "OverlappingHeight");
const QLatin1String settingsKey_OverlappingPosition(    "OverlappingPosition");
const QLatin1String settingsKey_UnitOfLength(           "UnitOfLength");

PosteRazorCore::PosteRazorCore(ImageLoaderInterface *imageLoader, QObject *parent)
    : QObject(parent)
    , m_imageLoader(imageLoader)
    , m_paperFormat(defaultValue_PaperFormat)
{
    Q_ASSERT(m_imageLoader);
}

unsigned int PosteRazorCore::imageBitsPerLineCount(int widthPixels, int bitPerPixel)
{
    return (widthPixels * bitPerPixel);
}

unsigned int PosteRazorCore::imageBytesPerLineCount(int widthPixels, int bitPerPixel)
{
    return (int)(ceil((qreal)imageBitsPerLineCount(widthPixels, bitPerPixel) / (qreal)8));
}

unsigned int PosteRazorCore::imageBytesCount(const QSize &size, int bitPerPixel)
{
    return imageBytesPerLineCount(size.width(), bitPerPixel) * size.height();
}

void PosteRazorCore::readSettings(const QSettings *settings)
{
    m_posterSizeMode               = (Types::PosterSizeModes)settings->value(settingsKey_PosterSizeMode, (int)m_posterSizeMode).toInt();
    m_posterDimension              = settings->value(settingsKey_PosterDimension, m_posterDimension).toDouble();
    m_posterDimensionIsWidth       = settings->value(settingsKey_PosterDimensionIsWidth, m_posterDimensionIsWidth).toBool();
    m_posterAlignment              = (Qt::Alignment)settings->value(settingsKey_PosterAlignment, (int)m_posterAlignment).toInt();
    m_usesCustomPaperSize           = settings->value(settingsKey_UseCustomPaperSize, m_usesCustomPaperSize).toBool();
    m_paperFormat                  = settings->value(settingsKey_PaperFormat, m_paperFormat).toString();
    if (!Types::paperFormats().contains(m_paperFormat))
        m_paperFormat = QLatin1String(defaultValue_PaperFormat);
    m_paperOrientation             = (QPrinter::Orientation)settings->value(settingsKey_PaperOrientation, (int)m_paperOrientation).toInt();
    m_paperBorderTop               = settings->value(settingsKey_PaperBorderTop, m_paperBorderTop).toDouble();
    m_paperBorderRight             = settings->value(settingsKey_PaperBorderRight, m_paperBorderRight).toDouble();
    m_paperBorderBottom            = settings->value(settingsKey_PaperBorderBottom, m_paperBorderBottom).toDouble();
    m_paperBorderLeft              = settings->value(settingsKey_PaperBorderLeft, m_paperBorderLeft).toDouble();
    m_customPaperWidth             = settings->value(settingsKey_CustomPaperWidth, m_customPaperWidth).toDouble();
    m_customPaperHeight            = settings->value(settingsKey_CustomPaperHeight, m_customPaperHeight).toDouble();
    m_overlappingWidth             = settings->value(settingsKey_OverlappingWidth, m_overlappingWidth).toDouble();
    m_overlappingHeight            = settings->value(settingsKey_OverlappingHeight, m_overlappingHeight).toDouble();
    m_overlappingPosition          = (Qt::Alignment)settings->value(settingsKey_OverlappingPosition, (int)m_overlappingPosition).toInt();
    m_unitOfLength                 = (Types::UnitsOfLength)settings->value(settingsKey_UnitOfLength, (int)m_unitOfLength).toInt();
}

void PosteRazorCore::writeSettings(QSettings *settings) const
{
    settings->setValue(settingsKey_PosterSizeMode, (int)m_posterSizeMode);
    settings->setValue(settingsKey_PosterDimension, m_posterDimension);
    settings->setValue(settingsKey_PosterDimensionIsWidth, m_posterDimensionIsWidth);
    settings->setValue(settingsKey_PosterAlignment, (int)m_posterAlignment);
    settings->setValue(settingsKey_UseCustomPaperSize, m_usesCustomPaperSize);
    settings->setValue(settingsKey_PaperFormat, m_paperFormat);
    settings->setValue(settingsKey_PaperOrientation, (int)m_paperOrientation);
    settings->setValue(settingsKey_PaperBorderTop, m_paperBorderTop);
    settings->setValue(settingsKey_PaperBorderRight, m_paperBorderRight);
    settings->setValue(settingsKey_PaperBorderBottom, m_paperBorderBottom);
    settings->setValue(settingsKey_PaperBorderLeft, m_paperBorderLeft);
    settings->setValue(settingsKey_CustomPaperWidth, m_customPaperWidth);
    settings->setValue(settingsKey_CustomPaperHeight, m_customPaperHeight);
    settings->setValue(settingsKey_OverlappingWidth, m_overlappingWidth);
    settings->setValue(settingsKey_OverlappingHeight, m_overlappingHeight);
    settings->setValue(settingsKey_OverlappingPosition, (int)m_overlappingPosition);
    settings->setValue(settingsKey_UnitOfLength, (int)m_unitOfLength);
}

qreal PosteRazorCore::convertDistanceToCm(qreal distance) const
{
    return Types::convertBetweenUnitsOfLength(distance, unitOfLength(), Types::UnitOfLengthCentimeter);
}

QSizeF PosteRazorCore::convertSizeToCm(const QSizeF &size) const
{
    return {convertDistanceToCm(size.width()), convertDistanceToCm(size.height())};
}

qreal PosteRazorCore::convertCmToDistance(qreal cm) const
{
    return Types::convertBetweenUnitsOfLength(cm, Types::UnitOfLengthCentimeter, unitOfLength());
}

QSizeF PosteRazorCore::convertCmToSize(const QSizeF &sizeInCm) const
{
    return {convertCmToDistance(sizeInCm.width()), convertCmToDistance(sizeInCm.height())};
}

bool PosteRazorCore::loadInputImage(const QString &imageFileName, QString &errorMessage)
{
    const bool success = m_imageLoader->loadInputImage(imageFileName, errorMessage);
    if (success)
        createPreviewImage();
    return success;
}

QString PosteRazorCore::fileName() const
{
    return m_imageLoader->fileName();
}

bool PosteRazorCore::isImageLoaded() const
{
    return m_imageLoader->isImageLoaded();
}

const QVector<QPair<QStringList, QString> > &PosteRazorCore::imageFormats() const
{
    return m_imageLoader->imageFormats();
}

const QString PosteRazorCore::imageIOLibraryName() const
{
    return m_imageLoader->libraryName();
}

const QString PosteRazorCore::imageIOLibraryAboutText() const
{
    return m_imageLoader->libraryAboutText();
}

QSize PosteRazorCore::inputImageSizePixels() const
{
    return m_imageLoader->sizePixels();
}

qreal PosteRazorCore::inputImageHorizontalDpi() const
{
    return m_imageLoader->horizontalDotsPerUnitOfLength(Types::UnitOfLengthInch);
}

qreal PosteRazorCore::inputImageVerticalDpi() const
{
    return m_imageLoader->verticalDotsPerUnitOfLength(Types::UnitOfLengthInch);
}

QSizeF PosteRazorCore::inputImageSize() const
{
    return m_imageLoader->size(m_unitOfLength);
}

int PosteRazorCore::inputImageBitsPerPixel() const
{
    return m_imageLoader->bitsPerPixel();
}

Types::ColorTypes PosteRazorCore::inputImageColorType() const
{
    return m_imageLoader->colorDataType();
}

void PosteRazorCore::setUnitOfLength(Types::UnitsOfLength unit)
{
    m_unitOfLength = unit;
}

Types::UnitsOfLength PosteRazorCore::unitOfLength() const
{
    return m_unitOfLength;
}

void PosteRazorCore::setPaperFormat(const QString &format)
{
    m_paperFormat = format;
}

void PosteRazorCore::setPaperOrientation(QPrinter::Orientation orientation)
{
    m_paperOrientation = orientation;
}

void PosteRazorCore::setPaperBorderTop(qreal border)
{
    m_paperBorderTop = convertDistanceToCm(border);
}

void PosteRazorCore::setPaperBorderRight(qreal border)
{
    m_paperBorderRight = convertDistanceToCm(border);
}

void PosteRazorCore::setPaperBorderBottom(qreal border)
{
    m_paperBorderBottom = convertDistanceToCm(border);
}

void PosteRazorCore::setPaperBorderLeft(qreal border)
{
    m_paperBorderLeft = convertDistanceToCm(border);
}

const QString PosteRazorCore::paperFormat() const
{
    return m_paperFormat;
}

QPrinter::Orientation PosteRazorCore::paperOrientation() const
{
    return m_paperOrientation;
}

qreal PosteRazorCore::paperBorderTop() const
{
    return qBound(.0, convertCmToDistance(m_paperBorderTop), maximalHorizontalPaperBorder());
}

qreal PosteRazorCore::paperBorderRight() const
{
    return qBound(.0, convertCmToDistance(m_paperBorderRight), maximalVerticalPaperBorder());
}

qreal PosteRazorCore::paperBorderBottom() const
{
    return qBound(.0, convertCmToDistance(m_paperBorderBottom), maximalHorizontalPaperBorder());
}

qreal PosteRazorCore::paperBorderLeft() const
{
    return qBound(.0, convertCmToDistance(m_paperBorderLeft), maximalVerticalPaperBorder());
}

qreal PosteRazorCore::maximalVerticalPaperBorder() const
{
    return paperSize().width() / 2.0 - convertCmToDistance(1.0);
}

qreal PosteRazorCore::maximalHorizontalPaperBorder() const
{
    return paperSize().height() / 2.0 - convertCmToDistance(1.0);
}

void PosteRazorCore::setCustomPaperWidth(qreal width)
{
    m_customPaperWidth = convertDistanceToCm(width);
}

void PosteRazorCore::setCustomPaperHeight(qreal height)
{
    m_customPaperHeight = convertDistanceToCm(height);
}

QSizeF PosteRazorCore::customPaperSize() const
{
    const qreal minimalPaperWidth = 4.0;
    const qreal minimalPaperHeight = minimalPaperWidth;
    const qreal maximalPaperWidth = 500.0; // 5 meter = maximum of a PDF page.
    const qreal maximalPaperHeight = maximalPaperWidth;
    return {
        convertCmToDistance(qBound(minimalPaperWidth, m_customPaperWidth, maximalPaperWidth)),
        convertCmToDistance(qBound(minimalPaperHeight, m_customPaperHeight, maximalPaperHeight))
    };
}

void PosteRazorCore::setUseCustomPaperSize(bool useIt)
{
    m_usesCustomPaperSize = useIt;
}

bool PosteRazorCore::usesCustomPaperSize() const
{
    return m_usesCustomPaperSize;
}

QSizeF PosteRazorCore::paperSize() const
{
    return usesCustomPaperSize() ? customPaperSize()
        : Types::paperSize(paperFormat(), paperOrientation(), m_unitOfLength);
}

QSizeF PosteRazorCore::printablePaperAreaSize() const
{
    return {
        paperSize().width() - paperBorderLeft() - paperBorderRight(),
        paperSize().height() - paperBorderTop() - paperBorderBottom()
    };
}

qreal PosteRazorCore::convertBetweenAbsoluteAndPagesPosterDimension(qreal dimension, bool pagesToAbsolute, bool width) const
{
    qreal posterDimension = dimension;

    const QSizeF printablePaperAreaSize = this->printablePaperAreaSize();
    const qreal printablePaperAreaDimension = convertDistanceToCm(width ? printablePaperAreaSize.width() : printablePaperAreaSize.height());
    const qreal overlappingDimension = convertDistanceToCm(width ? overlappingWidth() : overlappingHeight());

    if (pagesToAbsolute) {
        qreal posterDimensionAbsolute = 0;
        if (posterDimension >= 1.0) {
            posterDimension -= 1.0;
            posterDimensionAbsolute += printablePaperAreaDimension;
        } else {
            posterDimensionAbsolute = posterDimension * printablePaperAreaDimension;
            posterDimension = 0;
        }

        if (posterDimension > 0)
            posterDimensionAbsolute += (posterDimension * (printablePaperAreaDimension - overlappingDimension));

        posterDimension = posterDimensionAbsolute;
    } else {
        qreal posterDimensionPages = 0;
        if (posterDimension >= printablePaperAreaDimension) {
            posterDimension -= printablePaperAreaDimension;
            posterDimensionPages += 1.0;
        } else {
            posterDimensionPages = posterDimension / printablePaperAreaDimension;
            posterDimension = 0;
        }

        if (posterDimension > 0)
            posterDimensionPages += (posterDimension / (printablePaperAreaDimension - overlappingDimension));

        posterDimension = posterDimensionPages;
    }

    return posterDimension;
}

// Since m_posterDimension holds only height or width of the poster,
// here we calculate the other dimension considering the aspect ratio.
qreal PosteRazorCore::calculateOtherPosterDimension() const
{
    qreal otherDimension = 0;

    if (posterSizeMode() != Types::PosterSizeModePercentual) {
        const QSizeF inputImageSize = this->inputImageSize();
        const qreal sourceReference = m_posterDimensionIsWidth ? inputImageSize.width() : inputImageSize.height();
        const qreal targetReference = m_posterDimensionIsWidth ? inputImageSize.height() : inputImageSize.width();
        const qreal aspectRatio = sourceReference/targetReference;

        if (posterSizeMode() != Types::PosterSizeModePages) {
            otherDimension = m_posterDimension / aspectRatio;
        } else {
            const qreal sourceAbsolute = convertBetweenAbsoluteAndPagesPosterDimension(m_posterDimension, true, m_posterDimensionIsWidth);
            const qreal targetAbsolute = sourceAbsolute/aspectRatio;
            otherDimension = convertBetweenAbsoluteAndPagesPosterDimension(targetAbsolute, false, !m_posterDimensionIsWidth);
        }
    }
    else
        otherDimension = m_posterDimension;

    return otherDimension;
}

void PosteRazorCore::setPosterDimension(Types::PosterSizeModes mode, qreal dimension, bool dimensionIsWidth)
{
    setPosterSizeMode(mode);

    if (posterSizeMode() == Types::PosterSizeModeAbsolute)
        dimension = convertDistanceToCm(dimension);

    m_posterDimension = dimension;
    m_posterDimensionIsWidth = dimensionIsWidth;
}

void PosteRazorCore::setOverlappingWidth(qreal width)
{
    m_overlappingWidth = convertDistanceToCm(width);
}

void PosteRazorCore::setOverlappingHeight(qreal height)
{
    m_overlappingHeight = convertDistanceToCm(height);
}

qreal PosteRazorCore::overlappingWidth() const
{
    return qBound(.0, convertCmToDistance(m_overlappingWidth), maximalOverLappingWidth());
}

qreal PosteRazorCore::overlappingHeight() const
{
    return qBound(.0, convertCmToDistance(m_overlappingHeight), maximalOverLappingHeight());
}

qreal PosteRazorCore::maximalOverLappingWidth() const
{
    return paperSize().width() - paperBorderLeft() - paperBorderRight() - convertCmToDistance(1.0);
}

qreal PosteRazorCore::maximalOverLappingHeight() const
{
    return paperSize().height() - paperBorderTop() - paperBorderBottom() - convertCmToDistance(1.0);
}

void PosteRazorCore::setOverlappingPosition(Qt::Alignment position)
{
    m_overlappingPosition = position;
}

Qt::Alignment PosteRazorCore::overlappingPosition() const
{
    return m_overlappingPosition;
}

void PosteRazorCore::setPosterWidth(Types::PosterSizeModes mode, qreal width)
{
    setPosterDimension(mode, width, true);
}

void PosteRazorCore::setPosterHeight(Types::PosterSizeModes mode, qreal height)
{
    setPosterDimension(mode, height, false);
}

void PosteRazorCore::setPosterSizeMode(Types::PosterSizeModes mode)
{
    m_posterSizeMode = mode;
}

qreal PosteRazorCore::posterDimension(Types::PosterSizeModes mode, bool width) const
{
    qreal posterDimension = (width == m_posterDimensionIsWidth) ? m_posterDimension : calculateOtherPosterDimension();

    posterDimension = qMax(0.001 , posterDimension);

    // anything to convert?
    if (posterSizeMode() != mode){
        // These are needed for conversion from and to PosterSizeModePercentual
        const QSizeF inputImageSize = this->inputImageSize();
        const qreal inputImageDimension = convertDistanceToCm(width ? inputImageSize.width() : inputImageSize.height());

        // First convert to absolute size mode (cm)
        if (posterSizeMode() == Types::PosterSizeModePages) {
            posterDimension = convertBetweenAbsoluteAndPagesPosterDimension(posterDimension, true, width);
        } else if (posterSizeMode() == Types::PosterSizeModePercentual) {
            posterDimension *= inputImageDimension;
            posterDimension /= 100.0;
        }

        // Then convert to the wanted size mode
        if (mode == Types::PosterSizeModePages) {
            posterDimension = convertBetweenAbsoluteAndPagesPosterDimension(posterDimension, false, width);
        } else if (mode == Types::PosterSizeModePercentual) {
            posterDimension /= inputImageDimension;
            posterDimension *= 100.0;
        }
    }

    if (mode == Types::PosterSizeModeAbsolute)
        posterDimension = convertCmToDistance(posterDimension);

    return posterDimension;
}

QSizeF PosteRazorCore::posterSize(Types::PosterSizeModes mode) const
{
    return {posterDimension(mode, true), posterDimension(mode, false)};
}

Types::PosterSizeModes PosteRazorCore::posterSizeMode() const
{
    return m_posterSizeMode;
}

void PosteRazorCore::setPosterAlignment(Qt::Alignment alignment)
{
    m_posterAlignment = alignment;
}

void PosteRazorCore::createPreviewImage()
{
    createPreviewImage(QSize(1024, 768));
}

Qt::Alignment PosteRazorCore::posterAlignment() const
{
    return m_posterAlignment;
}

QSizeF PosteRazorCore::previewSize(const QSizeF &imageSize, const QSize &boxSize, bool enlargeToFit) const
{
    QSizeF result(imageSize);

    QSizeF boundedBoxSize(boxSize);
    if (!enlargeToFit)
        boundedBoxSize = boundedBoxSize.boundedTo(imageSize);

    result.scale(boundedBoxSize, Qt::KeepAspectRatio);
    return result;
}

QSizeF PosteRazorCore::inputImagePreviewSize(const QSize &boxSize) const
{
    return previewSize(inputImageSizePixels(), boxSize, false);
}

void PosteRazorCore::createPreviewImage(const QSize &size) const
{
    const QImage previewImage = m_imageLoader->imageAsRGB(inputImagePreviewSize(size).toSize());
    emit previewImageChanged(previewImage);
}

void PosteRazorCore::paintImageOnCanvas(PaintCanvasInterface *paintCanvas) const
{
    if (isImageLoaded()) {
        const QSizeF canvasSize = paintCanvas->size();
        const QSize inputImageSize = inputImageSizePixels();
        const QSizeF boxSize = previewSize(inputImageSize, canvasSize.toSize(), false);
        QPointF offset((canvasSize.width() - boxSize.width()) / 2, (canvasSize.height() - boxSize.height()) / 2);

        // If the image is not downscaled, make sure that the coordinates are integers in order
        // to prevent unneeded blurring
        if (canvasSize.width() >= inputImageSize.width() && canvasSize.height() >= inputImageSize.height()) {
            offset.setX(floor(offset.x()));
            offset.setY(floor(offset.y()));
        }

        paintCanvas->drawImage(QRectF(offset, boxSize));
    }
}

void PosteRazorCore::paintPaperOnCanvas(PaintCanvasInterface *paintCanvas, bool paintOverlapping) const
{
    const QSizeF canvasSize = paintCanvas->size();
    const QSizeF paperSize = this->paperSize();
    const QSizeF boxSize = previewSize(paperSize, canvasSize.toSize(), true);
    const QPointF offset((canvasSize.width() - boxSize.width()) / 2.0, (canvasSize.height() - boxSize.height()) / 2.0);
    const qreal UnitOfLengthToPixelfactor = boxSize.width()/paperSize.width();
    const qreal borderTop = paperBorderTop() * UnitOfLengthToPixelfactor;
    const qreal borderRight = paperBorderRight() * UnitOfLengthToPixelfactor;
    const qreal borderBottom = paperBorderBottom() * UnitOfLengthToPixelfactor;
    const qreal borderLeft = paperBorderLeft() * UnitOfLengthToPixelfactor;
    const QSizeF printableAreaSize(boxSize.width() - borderLeft - borderRight, boxSize.height() - borderTop - borderBottom);

    paintCanvas->drawFilledRect(QRectF(offset, boxSize), QColor(128, 128, 128));
    paintCanvas->drawFilledRect(QRectF(QPointF(borderLeft, borderTop) + offset, printableAreaSize), QColor(230, 230, 230));

    if (paintOverlapping) {
        const qreal overlappingWidth = this->overlappingWidth() * UnitOfLengthToPixelfactor;
        const qreal overlappingHeight = this->overlappingHeight() * UnitOfLengthToPixelfactor;
        const Qt::Alignment overlappingPosition = this->overlappingPosition();
        const qreal overlappingTop = (overlappingPosition & Qt::AlignTop) ?
            borderTop : boxSize.height() - borderBottom - overlappingHeight;
        const qreal overlappingLeft = (overlappingPosition & Qt::AlignLeft) ?
            borderLeft : boxSize.width() - borderRight - overlappingWidth;

        const QColor overlappingBrush(255, 128, 128);
        paintCanvas->drawFilledRect(QRectF(QPointF(borderLeft, overlappingTop) + offset, QSizeF(printableAreaSize.width(), overlappingHeight)), overlappingBrush);
        paintCanvas->drawFilledRect(QRectF(QPointF(overlappingLeft, borderTop) + offset, QSizeF(overlappingWidth, printableAreaSize.height())), overlappingBrush);
    }
}

void PosteRazorCore::paintPosterOnCanvasOverlapped(PaintCanvasInterface *paintCanvas) const
{
    const QSizeF canvasSize = paintCanvas->size();
    QSizeF pagePrintableAreaSize = printablePaperAreaSize();
    const QSizeF posterSizePages = posterSize(Types::PosterSizeModePages);
    const int pagesHorizontal = (int)ceil(posterSizePages.width());
    const int pagesVertical = (int)ceil(posterSizePages.height());
    const QSizeF posterSize(
        pagesHorizontal*pagePrintableAreaSize.width() - (pagesHorizontal-1)*overlappingWidth() + paperBorderLeft() + paperBorderRight(),
        pagesVertical*pagePrintableAreaSize.height() - (pagesVertical-1)*overlappingHeight() + paperBorderTop() + paperBorderBottom()
    );
    const QSizeF boxSize = previewSize(posterSize, canvasSize.toSize(), true);
    const QPointF offset((canvasSize.width() - boxSize.width()) / 2, (canvasSize.height() - boxSize.height()) / 2);
    const qreal UnitOfLengthToPixelfactor = boxSize.width()/posterSize.width();

    const qreal borderTop = paperBorderTop() * UnitOfLengthToPixelfactor;
    const qreal borderRight = paperBorderRight() * UnitOfLengthToPixelfactor;
    const qreal borderBottom = paperBorderBottom() * UnitOfLengthToPixelfactor;
    const qreal borderLeft = paperBorderLeft() * UnitOfLengthToPixelfactor;
    const QSizeF posterPrintableAreaSize(boxSize.width() - borderLeft - borderRight, boxSize.height() - borderTop - borderBottom);
    const QPointF posterPrintableAreaOrigin = QPointF(borderLeft, borderTop) + offset;
    const QRectF posterPrintableArea(posterPrintableAreaOrigin, posterPrintableAreaSize);

    paintCanvas->drawFilledRect(QRectF(offset, boxSize), QColor(128, 128, 128));
    paintCanvas->drawFilledRect(posterPrintableArea, QColor(230, 230, 230));

    const QSizeF posterSizeAbsolute = this->posterSize(Types::PosterSizeModeAbsolute);
    const QSizeF imageSize = posterSizeAbsolute * UnitOfLengthToPixelfactor;
    const Qt::Alignment alignment = posterAlignment();

    paintCanvas->drawImage(
        QRectF(
            QPointF(
                (
                    (alignment & Qt::AlignLeft) ? borderLeft
                    : (alignment & Qt::AlignHCenter) ? qBound(borderLeft, (boxSize.width() - imageSize.width()) / 2, borderLeft + posterPrintableAreaSize.width() - imageSize.width())
                    : (borderLeft + posterPrintableAreaSize.width() - imageSize.width())
                ) + offset.x(),
                (
                    (alignment & Qt::AlignTop) ? borderTop
                    : (alignment & Qt::AlignVCenter) ? qBound(borderTop, (boxSize.height() - imageSize.height()) / 2, borderTop + posterPrintableAreaSize.height() - imageSize.height())
                    : (borderTop + posterPrintableAreaSize.height() - imageSize.height())
                ) + offset.y()
            ),
            imageSize
        )
    );

    const qreal overlappingHeight = this->overlappingHeight() * UnitOfLengthToPixelfactor;
    const qreal overlappingWidth = this->overlappingWidth() * UnitOfLengthToPixelfactor;
    pagePrintableAreaSize *= UnitOfLengthToPixelfactor;

    const QColor overlappingColor(255, 128, 128, 128);
    qreal overlappingRectangleYPosition = borderTop;
    for (int pagesRow = 0; pagesRow < pagesVertical - 1; pagesRow++) {
        overlappingRectangleYPosition += pagePrintableAreaSize.height() - overlappingHeight;
        paintCanvas->drawFilledRect(QRectF(QPointF(0, overlappingRectangleYPosition) + offset, QSizeF(boxSize.width(), overlappingHeight)), overlappingColor);
    }

    qreal overlappingRectangleXPosition = borderLeft;
    for (int pagesColumn = 0; pagesColumn < pagesHorizontal - 1; pagesColumn++) {
        overlappingRectangleXPosition += pagePrintableAreaSize.width() - overlappingWidth;
        paintCanvas->drawFilledRect(QRectF(QPointF(overlappingRectangleXPosition, 0) + offset, QSizeF(overlappingWidth, boxSize.height())), overlappingColor);
    }

    const int fontSize = int(qMin(pagePrintableAreaSize.width() / 2.5, pagePrintableAreaSize.height() / 1.5));
    for (int pagesRow = 0; pagesRow < pagesVertical; ++pagesRow) {
        for (int pagesColumn = 0; pagesColumn < pagesHorizontal; ++pagesColumn) {
            const QPointF pagePrintableAreaOrigin = posterPrintableAreaOrigin + QPointF(
                pagesColumn * (pagePrintableAreaSize.width() - overlappingWidth),
                pagesRow * (pagePrintableAreaSize.height() - overlappingHeight)
            );
            const QRectF pageNumberArea = QRectF(pagePrintableAreaOrigin, pagePrintableAreaSize).adjusted(
                pagesColumn == 0 ? 0 : overlappingWidth,
                pagesRow == 0 ? 0 : overlappingHeight,
                pagesColumn == pagesHorizontal - 1 ? 0 : -overlappingWidth,
                pagesRow == pagesVertical - 1 ? 0 : -overlappingHeight
            );
            const int pageNumber = pagesRow * pagesHorizontal + pagesColumn + 1;
            paintCanvas->drawOverlayText(pageNumberArea.center(), Qt::AlignCenter, fontSize, QString::number(pageNumber));
        }
    }
}

void PosteRazorCore::paintPosterOnCanvasDivided(PaintCanvasInterface *paintCanvas) const
{
    Q_UNUSED(paintCanvas)
}

void PosteRazorCore::paintPosterOnCanvasPageWise(PaintCanvasInterface *paintCanvas, int page) const
{
    Q_UNUSED(paintCanvas)
    Q_UNUSED(page)
}

void PosteRazorCore::paintPosterPageOnCanvas(PaintCanvasInterface *paintCanvas, int page) const
{
    const QSizeF posterSizePages = posterSize(Types::PosterSizeModePages);
    const int columsCount = (int)(ceil(posterSizePages.width()));
    const int rowsCount = (int)(ceil(posterSizePages.height()));
    const int column = page % columsCount;
    const int row = page / columsCount;

    const QSizeF posterSizeAbsolute = posterSize(Types::PosterSizeModeAbsolute);
    const QSizeF posterImageSizeCm = convertSizeToCm(posterSizeAbsolute);
    const QSizeF printablePaperAreaSizeCm = convertSizeToCm(printablePaperAreaSize());
    const qreal overlappingWidthCm = convertDistanceToCm(overlappingWidth());
    const qreal overlappingHeightCm = convertDistanceToCm(overlappingHeight());
    const QSizeF printablePosterAreaSizeCm(
        columsCount * printablePaperAreaSizeCm.width() - (columsCount - 1) * overlappingWidthCm,
        rowsCount * printablePaperAreaSizeCm.height() - (rowsCount - 1) * overlappingHeightCm
    );
    const qreal borderTopCm = convertDistanceToCm(paperBorderTop());
    const qreal borderRightCm = convertDistanceToCm(paperBorderRight());
    const qreal borderBottomCm = convertDistanceToCm(paperBorderBottom());
    const qreal borderLeftCm = convertDistanceToCm(paperBorderLeft());
    const QSizeF posterTotalSizeCm(
        printablePosterAreaSizeCm.width() + borderLeftCm + borderRightCm,
        printablePosterAreaSizeCm.height() + borderTopCm + borderBottomCm
    );
    const Qt::Alignment alignment = posterAlignment();
    qreal imageOffsetFromLeftPosterBorderCm = (
        (alignment & Qt::AlignRight) ? posterTotalSizeCm.width() - posterImageSizeCm.width() - borderLeftCm
        : (alignment & Qt::AlignHCenter) ? (posterTotalSizeCm.width() - posterImageSizeCm.width()) / 2 - borderLeftCm
        : -borderLeftCm
    );
    imageOffsetFromLeftPosterBorderCm = qBound(.0, imageOffsetFromLeftPosterBorderCm, posterTotalSizeCm.width() - posterImageSizeCm.width() - borderLeftCm - borderRightCm);
    qreal imageOffsetFromTopPosterBorderCm = (
        (alignment & Qt::AlignBottom) ? posterTotalSizeCm.height() - posterImageSizeCm.height() - borderTopCm
        : (alignment & Qt::AlignVCenter) ? (posterTotalSizeCm.height() - posterImageSizeCm.height()) / 2 - borderTopCm
        : -borderTopCm
    );
    imageOffsetFromTopPosterBorderCm = qBound(.0, imageOffsetFromTopPosterBorderCm, posterTotalSizeCm.height() - posterImageSizeCm.height() - borderTopCm - borderBottomCm);
    const QPointF pageOffsetToImageFromTopLeftCm(
        column * (printablePaperAreaSizeCm.width()- overlappingWidthCm) - imageOffsetFromLeftPosterBorderCm,
        row * (printablePaperAreaSizeCm.height() - overlappingHeightCm) - imageOffsetFromTopPosterBorderCm
    );
    paintCanvas->drawImage(QRectF(-pageOffsetToImageFromTopLeftCm, posterImageSizeCm));
}

void PosteRazorCore::paintOnCanvas(PaintCanvasInterface *paintCanvas, const QVariant &options) const
{
    const QString state = options.toString();

    if (state == QLatin1String("image")) {
        paintImageOnCanvas(paintCanvas);
    } else if (state == QLatin1String("paper") || state == QLatin1String("overlapping")) {
        paintPaperOnCanvas(paintCanvas, state == QLatin1String("overlapping"));
    } else if (state.startsWith(QLatin1String("posterpage"))) {
        const int page = state.split(QLatin1Char(' ')).last().toInt();
        paintPosterPageOnCanvas(paintCanvas, page);
    } else if (state.startsWith(QLatin1String("poster"))) {
        const QStringList options = state.split(QLatin1Char(' '));
        if (options.at(1) == QLatin1String("overlapped")) {
            paintPosterOnCanvasOverlapped(paintCanvas);
        } else if (options.at(1) == QLatin1String("divided")) {
            paintPosterOnCanvasDivided(paintCanvas);
        } else if (options.at(1) == QLatin1String("pagewise")) {
            const int page = options.at(2).toInt();
            paintPosterOnCanvasPageWise(paintCanvas, page);
        } else {
            qFatal("Unimplemented poster mode in PosteRazorCore::paintOnCanvas().");
        }
    } else {
        qFatal("Unimplemented state in PosteRazorCore::paintOnCanvas().");
    }
}

int PosteRazorCore::savePoster(QIODevice *outputDevice) const
{
    int err = 0;

    const QSizeF posterSizePages = posterSize(Types::PosterSizeModePages);
    const QSizeF sizeCm = convertSizeToCm(printablePaperAreaSize());
    const int pagesCount = (int)(ceil(posterSizePages.width())) * (int)(ceil(posterSizePages.height()));
    const QSize imageSize = m_imageLoader->sizePixels();
    const QByteArray imageData = m_imageLoader->bits();

    PDFWriter pdfWriter;
    err = pdfWriter.startSaving(outputDevice, pagesCount, sizeCm.width(), sizeCm.height());
    if (!err) {
        if (m_imageLoader->isJpeg())
            err = pdfWriter.saveJpegImage(m_imageLoader->fileName(), imageSize, m_imageLoader->colorDataType());
        else
            err = pdfWriter.saveImage(imageData, imageSize, m_imageLoader->bitsPerPixel(), m_imageLoader->colorDataType(), m_imageLoader->colorTable());
    }

    if (!err) {
        for (int page = 0; page < pagesCount; page++) {
            pdfWriter.startPage();
            paintOnCanvas(&pdfWriter, QString::fromLatin1("posterpage %1").arg(page));
            pdfWriter.finishPage();
        }
        err = pdfWriter.finishSaving();
    }

    return err;
}
