PDF4QT/PdfForQtLib/sources/pdfdrawspacecontroller.cpp
2019-02-24 19:42:00 +01:00

773 lines
25 KiB
C++

// Copyright (C) 2019 Jakub Melka
//
// This file is part of PdfForQt.
//
// PdfForQt 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
// (at your option) any later version.
//
// PdfForQt 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 PDFForQt. If not, see <https://www.gnu.org/licenses/>.
#include "pdfdrawspacecontroller.h"
#include "pdfdrawwidget.h"
#include "pdfrenderer.h"
#include <QPainter>
namespace pdf
{
PDFDrawSpaceController::PDFDrawSpaceController(QObject* parent) :
QObject(parent),
m_document(nullptr),
m_pageLayoutMode(PageLayout::OneColumn),
m_verticalSpacingMM(5.0),
m_horizontalSpacingMM(1.0)
{
}
void PDFDrawSpaceController::setDocument(const PDFDocument* document)
{
if (document != m_document)
{
m_document = document;
recalculate();
}
}
void PDFDrawSpaceController::setPageLayout(PageLayout pageLayout)
{
if (m_pageLayoutMode != pageLayout)
{
m_pageLayoutMode = pageLayout;
recalculate();
}
}
QRectF PDFDrawSpaceController::getBlockBoundingRectangle(size_t blockIndex) const
{
if (blockIndex < m_blockItems.size())
{
return m_blockItems[blockIndex].blockRectMM;
}
return QRectF();
}
PDFDrawSpaceController::LayoutItems PDFDrawSpaceController::getLayoutItems(size_t blockIndex) const
{
LayoutItems result;
auto comparator = [](const LayoutItem& l, const LayoutItem& r)
{
return l.blockIndex < r.blockIndex;
};
Q_ASSERT(std::is_sorted(m_layoutItems.cbegin(), m_layoutItems.cend(), comparator));
LayoutItem templateItem;
templateItem.blockIndex = blockIndex;
auto range = std::equal_range(m_layoutItems.cbegin(), m_layoutItems.cend(), templateItem, comparator);
result.reserve(std::distance(range.first, range.second));
std::copy(range.first, range.second, std::back_inserter(result));
return result;
}
void PDFDrawSpaceController::recalculate()
{
if (!m_document)
{
clear(true);
return;
}
const PDFCatalog* catalog = m_document->getCatalog();
size_t pageCount = catalog->getPageCount();
// First, preserve page rotations. We assume the count of pages is the same as the document.
// Document should not be changed while viewing. If a new document is setted, then the draw
// space is cleared first.
std::vector<PageRotation> pageRotation(pageCount, PageRotation::None);
for (size_t i = 0; i < pageCount; ++i)
{
pageRotation[i] = catalog->getPage(i)->getPageRotation();
}
for (const LayoutItem& layoutItem : m_layoutItems)
{
pageRotation[layoutItem.pageIndex] = layoutItem.pageRotation;
}
// Clear the old draw space
clear(false);
static constexpr size_t INVALID_PAGE_INDEX = std::numeric_limits<size_t>::max();
// Places the pages on the left/right sides. Pages can be nullptr, but not both of them.
// Updates bounding rectangle.
auto placePagesLeftRight = [this, catalog, &pageRotation](PDFInteger blockIndex, size_t leftIndex, size_t rightIndex, PDFReal& yPos, QRectF& boundingRect)
{
PDFReal yPosAdvance = 0.0;
if (leftIndex != INVALID_PAGE_INDEX)
{
QSizeF pageSize = PDFPage::getRotatedBox(catalog->getPage(leftIndex)->getMediaBoxMM(), pageRotation[leftIndex]).size();
PDFReal xPos = -pageSize.width() - m_horizontalSpacingMM * 0.5;
QRectF rect(xPos, yPos, pageSize.width(), pageSize.height());
m_layoutItems.emplace_back(blockIndex, leftIndex, pageRotation[leftIndex], rect);
yPosAdvance = qMax(yPosAdvance, pageSize.height());
boundingRect = boundingRect.united(rect);
}
if (rightIndex != INVALID_PAGE_INDEX)
{
QSizeF pageSize = PDFPage::getRotatedBox(catalog->getPage(rightIndex)->getMediaBoxMM(), pageRotation[rightIndex]).size();
PDFReal xPos = m_horizontalSpacingMM * 0.5;
QRectF rect(xPos, yPos, pageSize.width(), pageSize.height());
m_layoutItems.emplace_back(blockIndex, rightIndex, pageRotation[rightIndex], rect);
yPosAdvance = qMax(yPosAdvance, pageSize.height());
boundingRect = boundingRect.united(rect);
}
if (yPosAdvance > 0.0)
{
yPos += yPosAdvance + m_verticalSpacingMM;
}
};
// Generates block with pages using page indices. If generateBlocks is true, then
// for each pair of pages, single block is generated, otherwise block containing all
// pages is generated.
auto placePagesLeftRightByIndices = [this, &placePagesLeftRight](const std::vector<size_t>& indices, bool generateBlocks)
{
Q_ASSERT(indices.size() % 2 == 0);
PDFReal yPos = 0.0;
PDFInteger blockIndex = 0;
QRectF boundingRectangle;
size_t count = indices.size() / 2;
for (size_t i = 0; i < count; ++i)
{
const size_t leftPageIndex = indices[2 * i];
const size_t rightPageIndex = indices[2 * i + 1];
placePagesLeftRight(blockIndex, leftPageIndex, rightPageIndex, yPos, boundingRectangle);
if (generateBlocks)
{
m_blockItems.emplace_back(boundingRectangle);
// Clear the old data
yPos = 0.0;
++blockIndex;
boundingRectangle = QRectF();
}
}
if (!generateBlocks)
{
// Generate single block for all layed out pages
m_blockItems.emplace_back(boundingRectangle);
}
};
switch (m_pageLayoutMode)
{
case PageLayout::SinglePage:
{
// Each block contains single page
m_layoutItems.reserve(pageCount);
m_blockItems.reserve(pageCount);
// Pages can have different size, so we center them around the center.
// Block size will equal to the page size.
for (size_t i = 0; i < pageCount; ++i)
{
QSizeF pageSize = PDFPage::getRotatedBox(catalog->getPage(i)->getMediaBoxMM(), pageRotation[i]).size();
QRectF rect(-pageSize.width() * 0.5, -pageSize.height() * 0.5, pageSize.width(), pageSize.height());
m_layoutItems.emplace_back(i, i, pageRotation[i], rect);
m_blockItems.emplace_back(rect);
}
break;
}
case PageLayout::OneColumn:
{
// Single block, one column
m_layoutItems.reserve(pageCount);
m_blockItems.reserve(1);
PDFReal yPos = 0.0;
QRectF boundingRectangle;
for (size_t i = 0; i < pageCount; ++i)
{
// Top of current page is at yPos.
QSizeF pageSize = PDFPage::getRotatedBox(catalog->getPage(i)->getMediaBoxMM(), pageRotation[i]).size();
QRectF rect(-pageSize.width() * 0.5, yPos, pageSize.width(), pageSize.height());
m_layoutItems.emplace_back(0, i, pageRotation[i], rect);
yPos += pageSize.height() + m_verticalSpacingMM;
boundingRectangle = boundingRectangle.united(rect);
}
// Insert the single block with union of bounding rectangles
m_blockItems.emplace_back(boundingRectangle);
break;
}
case PageLayout::TwoColumnLeft:
{
// Pages with number 1, 3, 5, ... are on the left, 2, 4, 6 are on the right.
// Page indices are numbered from 0, so pages 0, 2, 4 will be on the left,
// 1, 3, 5 will be on the right.
// For purposes or paging, "left" pages will be on the left side of y axis (negative x axis),
// the "right" pages will be on the right side of y axis (positive x axis).
m_layoutItems.reserve(pageCount);
m_blockItems.reserve(1);
std::vector<size_t> pageIndices(pageCount, INVALID_PAGE_INDEX);
std::iota(pageIndices.begin(), pageIndices.end(), static_cast<size_t>(0));
if (pageIndices.size() % 2 == 1)
{
pageIndices.push_back(INVALID_PAGE_INDEX);
}
placePagesLeftRightByIndices(pageIndices, false);
break;
}
case PageLayout::TwoColumnRight:
{
// Similar to previous case, but page sequence start on the right.
m_layoutItems.reserve(pageCount);
m_blockItems.reserve(1);
std::vector<size_t> pageIndices(pageCount + 1, INVALID_PAGE_INDEX);
std::iota(std::next(pageIndices.begin()), pageIndices.end(), static_cast<size_t>(0));
if (pageIndices.size() % 2 == 1)
{
pageIndices.push_back(INVALID_PAGE_INDEX);
}
placePagesLeftRightByIndices(pageIndices, false);
break;
}
case PageLayout::TwoPagesLeft:
{
m_layoutItems.reserve(pageCount);
m_blockItems.reserve((pageCount / 2) + (pageCount % 2));
std::vector<size_t> pageIndices(pageCount, INVALID_PAGE_INDEX);
std::iota(pageIndices.begin(), pageIndices.end(), static_cast<size_t>(0));
if (pageIndices.size() % 2 == 1)
{
pageIndices.push_back(INVALID_PAGE_INDEX);
}
placePagesLeftRightByIndices(pageIndices, true);
break;
}
case PageLayout::TwoPagesRight:
{
m_layoutItems.reserve(pageCount);
m_blockItems.reserve((pageCount / 2) + (pageCount % 2));
std::vector<size_t> pageIndices(pageCount + 1, INVALID_PAGE_INDEX);
std::iota(std::next(pageIndices.begin()), pageIndices.end(), static_cast<size_t>(0));
if (pageIndices.size() % 2 == 1)
{
pageIndices.push_back(INVALID_PAGE_INDEX);
}
placePagesLeftRightByIndices(pageIndices, true);
break;
}
default:
{
Q_ASSERT(false);
break;
}
}
emit drawSpaceChanged();
}
void PDFDrawSpaceController::clear(bool emitSignal)
{
m_layoutItems.clear();
m_blockItems.clear();
if (emitSignal)
{
emit drawSpaceChanged();
}
}
PDFDrawWidgetProxy::PDFDrawWidgetProxy(QObject* parent) :
QObject(parent),
m_updateDisabled(false),
m_currentBlock(INVALID_BLOCK_INDEX),
m_pixelPerMM(PDF_DEFAULT_DPMM),
m_zoom(1.0),
m_pixelToDeviceSpaceUnit(0.0),
m_deviceSpaceUnitToPixel(0.0),
m_verticalOffset(0),
m_horizontalOffset(0),
m_controller(nullptr),
m_widget(nullptr),
m_horizontalScrollbar(nullptr),
m_verticalScrollbar(nullptr)
{
m_controller = new PDFDrawSpaceController(this);
connect(m_controller, &PDFDrawSpaceController::drawSpaceChanged, this, &PDFDrawWidgetProxy::update);
}
void PDFDrawWidgetProxy::setDocument(const PDFDocument* document)
{
m_controller->setDocument(document);
}
void PDFDrawWidgetProxy::init(PDFWidget* widget)
{
m_widget = widget->getDrawWidget();
m_horizontalScrollbar = widget->getHorizontalScrollbar();
m_verticalScrollbar = widget->getVerticalScrollbar();
connect(m_horizontalScrollbar, &QScrollBar::valueChanged, this, &PDFDrawWidgetProxy::onHorizontalScrollbarValueChanged);
connect(m_verticalScrollbar, &QScrollBar::valueChanged, this, &PDFDrawWidgetProxy::onVerticalScrollbarValueChanged);
connect(this, &PDFDrawWidgetProxy::drawSpaceChanged, m_widget, QOverload<void>::of(&PDFDrawWidget::update));
// We must update the draw space - widget has been set
update();
}
void PDFDrawWidgetProxy::update()
{
if (m_updateDisabled)
{
return;
}
PDFBoolGuard guard(m_updateDisabled);
Q_ASSERT(m_widget);
Q_ASSERT(m_horizontalScrollbar);
Q_ASSERT(m_verticalScrollbar);
// First, we must calculate pixel per mm ratio to obtain DPMM (device pixel per mm),
// we also assume, that zoom is correctly set.
m_pixelPerMM = static_cast<PDFReal>(m_widget->width()) / static_cast<PDFReal>(m_widget->widthMM());
Q_ASSERT(m_zoom > 0.0);
Q_ASSERT(m_pixelPerMM > 0.0);
m_deviceSpaceUnitToPixel = m_pixelPerMM * m_zoom;
m_pixelToDeviceSpaceUnit = 1.0 / m_deviceSpaceUnitToPixel;
m_layout.clear();
// Switch to the first block, if we haven't selected any, otherwise fix active
// block item (select first block available).
if (m_controller->getBlockCount() > 0)
{
if (m_currentBlock == INVALID_BLOCK_INDEX)
{
m_currentBlock = 0;
}
else
{
m_currentBlock = qBound<PDFInteger>(0, m_currentBlock, m_controller->getBlockCount());
}
}
else
{
m_currentBlock = INVALID_BLOCK_INDEX;
}
// Then, create pixel size layout of the pages using the draw space controller
QRectF rectangle = m_controller->getBlockBoundingRectangle(m_currentBlock);
if (rectangle.isValid())
{
// We must have a valid block
PDFDrawSpaceController::LayoutItems items = m_controller->getLayoutItems(m_currentBlock);
m_layout.items.reserve(items.size());
for (const PDFDrawSpaceController::LayoutItem& item : items)
{
m_layout.items.emplace_back(item.pageIndex, item.pageRotation, fromDeviceSpace(item.pageRectMM).toRect());
}
m_layout.blockRect = fromDeviceSpace(rectangle).toRect();
}
QSize blockSize = m_layout.blockRect.size();
QSize widgetSize = m_widget->size();
// Horizontal scrollbar
const int horizontalDifference = blockSize.width() - widgetSize.width();
if (horizontalDifference > 0)
{
m_horizontalScrollbar->setVisible(true);
m_horizontalScrollbar->setMinimum(0);
m_horizontalScrollbar->setMaximum(horizontalDifference);
m_horizontalOffsetRange = Range<PDFInteger>(-horizontalDifference, 0);
m_horizontalOffset = m_horizontalOffsetRange.bound(m_horizontalOffset);
m_horizontalScrollbar->setValue(-m_horizontalOffset);
}
else
{
// We do not need the horizontal scrollbar, because block can be draw onto widget entirely.
// We set the offset to the half of available empty space.
m_horizontalScrollbar->setVisible(false);
m_horizontalOffset = -horizontalDifference / 2;
m_horizontalOffsetRange = Range<PDFInteger>(m_horizontalOffset);
}
// Vertical scrollbar - has two meanings, in block mode, it switches between blocks,
// in continuous mode, it controls the vertical offset.
if (isBlockMode())
{
size_t blockCount = m_controller->getBlockCount();
if (blockCount > 0)
{
Q_ASSERT(m_currentBlock != INVALID_BLOCK_INDEX);
m_verticalScrollbar->setVisible(blockCount > 1);
m_verticalScrollbar->setMinimum(0);
m_verticalScrollbar->setMaximum(static_cast<int>(blockCount - 1));
m_verticalScrollbar->setValue(static_cast<int>(m_currentBlock));
m_verticalScrollbar->setSingleStep(1);
m_verticalScrollbar->setPageStep(1);
}
else
{
Q_ASSERT(m_currentBlock == INVALID_BLOCK_INDEX);
m_verticalScrollbar->setVisible(false);
}
// We must fix case, when we can display everything on the widget (we have
// enough space). Then we will center the page on the widget.
const int verticalDifference = blockSize.height() - widgetSize.height();
if (verticalDifference < 0)
{
m_verticalOffset = -verticalDifference / 2;
m_verticalOffsetRange = Range<PDFInteger>(m_verticalOffset);
}
else
{
m_verticalOffsetRange = Range<PDFInteger>(-verticalDifference, 0);
m_verticalOffset = m_verticalOffsetRange.bound(m_verticalOffset);
}
}
else
{
const int verticalDifference = blockSize.height() - widgetSize.height();
if (verticalDifference > 0)
{
m_verticalScrollbar->setVisible(true);
m_verticalScrollbar->setMinimum(0);
m_verticalScrollbar->setMaximum(verticalDifference);
// We must also calculate single step/page step. Because pages can have different size,
// we use first page to compute page step.
if (!m_layout.items.empty())
{
const LayoutItem& item = m_layout.items.front();
const int pageStep = qMax(item.pageRect.height(), 1);
const int singleStep = qMax(pageStep / 10, 1);
m_verticalScrollbar->setPageStep(pageStep);
m_verticalScrollbar->setSingleStep(singleStep);
}
m_verticalOffsetRange = Range<PDFInteger>(-verticalDifference, 0);
m_verticalOffset = m_verticalOffsetRange.bound(m_verticalOffset);
m_verticalScrollbar->setValue(-m_verticalOffset);
}
else
{
m_verticalScrollbar->setVisible(false);
m_verticalOffset = -verticalDifference / 2;
m_verticalOffsetRange = Range<PDFInteger>(m_verticalOffset);
}
}
emit drawSpaceChanged();
}
void PDFDrawWidgetProxy::draw(QPainter* painter, QRect rect)
{
painter->fillRect(rect, Qt::lightGray);
// Iterate trough pages and display them on the painter device
for (const LayoutItem& item : m_layout.items)
{
// The offsets m_horizontalOffset and m_verticalOffset are offsets to the
// topleft point of the block. But block maybe doesn't start at (0, 0),
// so we must also use translation from the block beginning.
QRect placedRect = item.pageRect.translated(m_horizontalOffset - m_layout.blockRect.left(), m_verticalOffset - m_layout.blockRect.top());
if (placedRect.intersects(rect))
{
// Clear the page space by white color
painter->fillRect(placedRect, Qt::white);
PDFRenderer renderer(m_controller->getDocument());
QList<PDFRenderError> errors = renderer.render(painter, placedRect, item.pageIndex);
if (!errors.empty())
{
emit renderingError(item.pageIndex, errors);
}
}
}
}
std::vector<PDFInteger> PDFDrawWidgetProxy::getPagesIntersectingRect(QRect rect) const
{
std::vector<PDFInteger> pages;
// We assume, that no more, than 32 pages will be displayed in the rectangle
pages.reserve(32);
// Iterate trough pages, place them and test, if they intersects with rectangle
for (const LayoutItem& item : m_layout.items)
{
// The offsets m_horizontalOffset and m_verticalOffset are offsets to the
// topleft point of the block. But block maybe doesn't start at (0, 0),
// so we must also use translation from the block beginning.
QRect placedRect = item.pageRect.translated(m_horizontalOffset - m_layout.blockRect.left(), m_verticalOffset - m_layout.blockRect.top());
if (placedRect.intersects(rect))
{
pages.push_back(item.pageIndex);
}
}
return pages;
}
void PDFDrawWidgetProxy::performOperation(Operation operation)
{
switch (operation)
{
case NavigateDocumentStart:
{
if (m_verticalScrollbar->isVisible())
{
m_verticalScrollbar->setValue(0);
}
break;
}
case NavigateDocumentEnd:
{
if (m_verticalScrollbar->isVisible())
{
m_verticalScrollbar->setValue(m_verticalScrollbar->maximum());
}
break;
}
case NavigateNextPage:
{
if (m_verticalScrollbar->isVisible())
{
m_verticalScrollbar->setValue(m_verticalScrollbar->value() + m_verticalScrollbar->pageStep());
}
break;
}
case NavigatePreviousPage:
{
if (m_verticalScrollbar->isVisible())
{
m_verticalScrollbar->setValue(m_verticalScrollbar->value() - m_verticalScrollbar->pageStep());
}
break;
}
case NavigateNextStep:
{
if (m_verticalScrollbar->isVisible())
{
m_verticalScrollbar->setValue(m_verticalScrollbar->value() + m_verticalScrollbar->singleStep());
}
break;
}
case NavigatePreviousStep:
{
if (m_verticalScrollbar->isVisible())
{
m_verticalScrollbar->setValue(m_verticalScrollbar->value() - m_verticalScrollbar->singleStep());
}
break;
}
default:
{
Q_ASSERT(false);
break;
}
}
}
void PDFDrawWidgetProxy::scrollByPixels(QPoint offset)
{
setHorizontalOffset(m_horizontalOffset + offset.x());
setVerticalOffset(m_verticalOffset + offset.y());
}
void PDFDrawWidgetProxy::zoom(PDFReal zoom)
{
const PDFReal clampedZoom = qBound(MIN_ZOOM, zoom, MAX_ZOOM);
if (m_zoom != clampedZoom)
{
const PDFReal oldHorizontalOffsetMM = m_horizontalOffset * m_pixelToDeviceSpaceUnit;
const PDFReal oldVerticalOffsetMM = m_verticalOffset * m_pixelToDeviceSpaceUnit;
m_zoom = clampedZoom;
update();
// Try to restore offsets, so we are in the same place
setHorizontalOffset(oldHorizontalOffsetMM * m_deviceSpaceUnitToPixel);
setVerticalOffset(oldVerticalOffsetMM * m_deviceSpaceUnitToPixel);
}
}
void PDFDrawWidgetProxy::setPageLayout(PageLayout pageLayout)
{
if (getPageLayout() != pageLayout)
{
m_controller->setPageLayout(pageLayout);
emit pageLayoutChanged();
}
}
QRectF PDFDrawWidgetProxy::fromDeviceSpace(const QRectF& rect) const
{
Q_ASSERT(rect.isValid());
return QRectF(rect.left() * m_deviceSpaceUnitToPixel,
rect.top() * m_deviceSpaceUnitToPixel,
rect.width() * m_deviceSpaceUnitToPixel,
rect.height() * m_deviceSpaceUnitToPixel);
}
bool PDFDrawWidgetProxy::isBlockMode() const
{
switch (m_controller->getPageLayout())
{
case PageLayout::OneColumn:
case PageLayout::TwoColumnLeft:
case PageLayout::TwoColumnRight:
return false;
case PageLayout::SinglePage:
case PageLayout::TwoPagesLeft:
case PageLayout::TwoPagesRight:
return true;
}
Q_ASSERT(false);
return false;
}
void PDFDrawWidgetProxy::onHorizontalScrollbarValueChanged(int value)
{
if (!m_updateDisabled && !m_horizontalScrollbar->isHidden())
{
setHorizontalOffset(-value);
}
}
void PDFDrawWidgetProxy::onVerticalScrollbarValueChanged(int value)
{
if (!m_updateDisabled && !m_verticalScrollbar->isHidden())
{
if (isBlockMode())
{
setBlockIndex(value);
}
else
{
setVerticalOffset(-value);
}
}
}
void PDFDrawWidgetProxy::setHorizontalOffset(int value)
{
const PDFInteger horizontalOffset = m_horizontalOffsetRange.bound(value);
if (m_horizontalOffset != horizontalOffset)
{
m_horizontalOffset = horizontalOffset;
updateHorizontalScrollbarFromOffset();
emit drawSpaceChanged();
}
}
void PDFDrawWidgetProxy::setVerticalOffset(int value)
{
const PDFInteger verticalOffset = m_verticalOffsetRange.bound(value);
if (m_verticalOffset != verticalOffset)
{
m_verticalOffset = verticalOffset;
updateVerticalScrollbarFromOffset();
emit drawSpaceChanged();
}
}
void PDFDrawWidgetProxy::setBlockIndex(int index)
{
if (m_currentBlock != index)
{
m_currentBlock = static_cast<size_t>(index);
update();
}
}
void PDFDrawWidgetProxy::updateHorizontalScrollbarFromOffset()
{
if (!m_horizontalScrollbar->isHidden())
{
PDFBoolGuard guard(m_updateDisabled);
m_horizontalScrollbar->setValue(-m_horizontalOffset);
}
}
void PDFDrawWidgetProxy::updateVerticalScrollbarFromOffset()
{
if (!m_verticalScrollbar->isHidden() && !isBlockMode())
{
PDFBoolGuard guard(m_updateDisabled);
m_verticalScrollbar->setValue(-m_verticalOffset);
}
}
} // namespace pdf