// Copyright (C) 2021 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 . #include "utils.h" #include "pdfutils.h" #include "pdfwidgetutils.h" #include "pdfpainterutils.h" #include "pdfdocumentbuilder.h" #include namespace pdfdocdiff { void ComparedDocumentMapper::update(ComparedDocumentMapper::Mode mode, bool filterDifferences, const pdf::PDFDiffResult& diff, const pdf::PDFDocument* leftDocument, const pdf::PDFDocument* rightDocument, const pdf::PDFDocument* currentDocument) { m_layout.clear(); m_leftPageIndices.clear(); m_rightPageIndices.clear(); m_allLeft = false; m_allRight = false; if (!leftDocument || !rightDocument || !currentDocument) { return; } // Jakub Melka pdf::PDFDiffResult::PageSequence pageSequence = diff.getPageSequence(); const bool isEmpty = pageSequence.empty(); if (filterDifferences) { pdf::PDFDiffResult::PageSequence filteredPageSequence; std::vector leftPageIndices = diff.getChangedLeftPageIndices(); std::vector rightPageIndices = diff.getChangedRightPageIndices(); for (const pdf::PDFDiffResult::PageSequenceItem& item : pageSequence) { const bool isLeftModified = std::binary_search(leftPageIndices.cbegin(), leftPageIndices.cend(), item.leftPage); const bool isRightModified = std::binary_search(rightPageIndices.cbegin(), rightPageIndices.cend(), item.rightPage); if (isLeftModified || isRightModified) { filteredPageSequence.push_back(item); } } pageSequence = std::move(filteredPageSequence); } switch (mode) { case ComparedDocumentMapper::Mode::Left: { Q_ASSERT(leftDocument == currentDocument); m_allLeft = true; double yPos = 0.0; const pdf::PDFCatalog* catalog = leftDocument->getCatalog(); if (isEmpty) { // Just copy all pages const size_t pageCount = catalog->getPageCount(); for (size_t i = 0; i < pageCount; ++i) { QSizeF pageSize = catalog->getPage(i)->getRotatedMediaBoxMM().size(); QRectF rect(-pageSize.width() * 0.5, yPos, pageSize.width(), pageSize.height()); m_layout.emplace_back(0, i, -1, rect); yPos += pageSize.height() + 5; } } else { for (const pdf::PDFDiffResult::PageSequenceItem& item : pageSequence) { if (item.leftPage == -1) { continue; } QSizeF pageSize = catalog->getPage(item.leftPage)->getRotatedMediaBoxMM().size(); QRectF rect(-pageSize.width() * 0.5, yPos, pageSize.width(), pageSize.height()); m_layout.emplace_back(0, item.leftPage, -1, rect); yPos += pageSize.height() + 5; } } break; } case ComparedDocumentMapper::Mode::Right: { Q_ASSERT(rightDocument == currentDocument); m_allRight = true; double yPos = 0.0; const pdf::PDFCatalog* catalog = rightDocument->getCatalog(); if (isEmpty) { // Just copy all pages const size_t pageCount = catalog->getPageCount(); for (size_t i = 0; i < pageCount; ++i) { QSizeF pageSize = catalog->getPage(i)->getRotatedMediaBoxMM().size(); QRectF rect(-pageSize.width() * 0.5, yPos, pageSize.width(), pageSize.height()); m_layout.emplace_back(0, i, -1, rect); yPos += pageSize.height() + 5; } } else { for (const pdf::PDFDiffResult::PageSequenceItem& item : pageSequence) { if (item.rightPage == -1) { continue; } QSizeF pageSize = catalog->getPage(item.rightPage)->getRotatedMediaBoxMM().size(); QRectF rect(-pageSize.width() * 0.5, yPos, pageSize.width(), pageSize.height()); m_layout.emplace_back(0, item.rightPage, -1, rect); yPos += pageSize.height() + 5; } } break; } case ComparedDocumentMapper::Mode::Combined: case ComparedDocumentMapper::Mode::Overlay: { double yPos = 0.0; const pdf::PDFCatalog* catalog = currentDocument->getCatalog(); pdf::PDFInteger offset = leftDocument->getCatalog()->getPageCount(); for (const pdf::PDFDiffResult::PageSequenceItem& item : pageSequence) { double yAdvance = 0.0; if (item.leftPage != -1) { QSizeF pageSize = catalog->getPage(item.leftPage)->getRotatedMediaBoxMM().size(); QRectF rect; pdf::PDFInteger groupIndex = -1; if (mode == ComparedDocumentMapper::Mode::Combined) { rect = QRectF(-pageSize.width() - 5, yPos, pageSize.width(), pageSize.height()); } else { if (item.rightPage != -1) { groupIndex = 1; } rect = QRectF(-pageSize.width() * 0.5, yPos, pageSize.width(), pageSize.height()); } m_layout.emplace_back(0, item.leftPage, groupIndex, rect); yAdvance = pageSize.height() + 5; m_leftPageIndices[item.leftPage] = item.leftPage; } if (item.rightPage != -1) { pdf::PDFInteger rightPageIndex = item.rightPage + offset; QSizeF pageSize = catalog->getPage(rightPageIndex)->getRotatedMediaBoxMM().size(); QRectF rect; pdf::PDFInteger groupIndex = -1; if (mode == ComparedDocumentMapper::Mode::Combined) { rect = QRectF(5, yPos, pageSize.width(), pageSize.height()); } else { if (item.leftPage != -1) { groupIndex = 2; } rect = QRectF(-pageSize.width() * 0.5, yPos, pageSize.width(), pageSize.height()); } m_layout.emplace_back(0, rightPageIndex, groupIndex, rect); yAdvance = qMax(yAdvance, pageSize.height() + 5); m_rightPageIndices[rightPageIndex] = item.rightPage; } yPos += yAdvance; } break; } default: Q_ASSERT(false); break; } } pdf::PDFInteger ComparedDocumentMapper::getLeftPageIndex(pdf::PDFInteger pageIndex) const { if (m_allLeft) { return pageIndex; } auto it = m_leftPageIndices.find(pageIndex); if (it != m_leftPageIndices.cend()) { return it->second; } return -1; } pdf::PDFInteger ComparedDocumentMapper::getRightPageIndex(pdf::PDFInteger pageIndex) const { if (m_allRight) { return pageIndex; } auto it = m_rightPageIndices.find(pageIndex); if (it != m_rightPageIndices.cend()) { return it->second; } return -1; } pdf::PDFInteger ComparedDocumentMapper::getPageIndexFromLeftPageIndex(pdf::PDFInteger leftPageIndex) const { if (m_allLeft) { return leftPageIndex; } for (const auto& indexItem : m_leftPageIndices) { if (indexItem.second == leftPageIndex) { return indexItem.first; } } return -1; } pdf::PDFInteger ComparedDocumentMapper::getPageIndexFromRightPageIndex(pdf::PDFInteger rightPageIndex) const { if (m_allRight) { return rightPageIndex; } for (const auto& indexItem : m_rightPageIndices) { if (indexItem.second == rightPageIndex) { return indexItem.first; } } return -1; } DifferencesDrawInterface::DifferencesDrawInterface(const Settings* settings, const ComparedDocumentMapper* mapper, const pdf::PDFDiffResult* diffResult) : m_settings(settings), m_mapper(mapper), m_diffResult(diffResult) { } void DifferencesDrawInterface::drawPage(QPainter* painter, pdf::PDFInteger pageIndex, const pdf::PDFPrecompiledPage* compiledPage, pdf::PDFTextLayoutGetter& layoutGetter, const QTransform& pagePointToDevicePointMatrix, QList& errors) const { Q_UNUSED(compiledPage); Q_UNUSED(layoutGetter); Q_UNUSED(errors); if (!m_settings->displayDifferences) { return; } const size_t differencesCount = m_diffResult->getDifferencesCount(); const pdf::PDFInteger leftPageIndex = m_mapper->getLeftPageIndex(pageIndex); const pdf::PDFInteger rightPageIndex = m_mapper->getRightPageIndex(pageIndex); std::optional pageMoveIndex; if (leftPageIndex != -1) { for (size_t i = 0; i < differencesCount; ++i) { auto leftRectangles = m_diffResult->getLeftRectangles(i); for (auto it = leftRectangles.first; it != leftRectangles.second; ++it) { const auto& item = *it; if (item.first == leftPageIndex) { QColor color = getColorForIndex(i); drawRectangle(painter, pagePointToDevicePointMatrix, item.second, color); drawMarker(painter, pagePointToDevicePointMatrix, item.second, color, true); } } if (m_diffResult->isPageMoveAddRemoveDifference(i) && m_diffResult->getLeftPage(i) == leftPageIndex) { pageMoveIndex = i; } } } if (rightPageIndex != -1) { for (size_t i = 0; i < differencesCount; ++i) { auto rightRectangles = m_diffResult->getRightRectangles(i); for (auto it = rightRectangles.first; it != rightRectangles.second; ++it) { const auto& item = *it; if (item.first == rightPageIndex) { QColor color = getColorForIndex(i); drawRectangle(painter, pagePointToDevicePointMatrix, item.second, color); drawMarker(painter, pagePointToDevicePointMatrix, item.second, color, false); } } if (m_diffResult->isPageMoveAddRemoveDifference(i) && m_diffResult->getRightPage(i) == rightPageIndex) { pageMoveIndex = i; } } } if (pageMoveIndex) { QString text; switch (m_diffResult->getType(*pageMoveIndex)) { case pdf::PDFDiffResult::Type::PageAdded: text = " + "; break; case pdf::PDFDiffResult::Type::PageRemoved: text = " - "; break; case pdf::PDFDiffResult::Type::PageMoved: text = QString("%1🠖%2").arg(m_diffResult->getLeftPage(*pageMoveIndex) + 1).arg(m_diffResult->getRightPage(*pageMoveIndex) + 1); break; default: Q_ASSERT(false); break; } QColor color = getColorForIndex(*pageMoveIndex); QPointF targetPoint = pagePointToDevicePointMatrix.map(QPointF(5, 5)); pdf::PDFPainterHelper::drawBubble(painter, targetPoint.toPoint(), color, text, Qt::AlignRight | Qt::AlignTop); } } QColor DifferencesDrawInterface::getColorForIndex(size_t index) const { QColor color; const size_t resultIndex = index; if (m_diffResult->isReplaceDifference(resultIndex)) { color = m_settings->colorReplaced; } else if (m_diffResult->isRemoveDifference(resultIndex)) { color = m_settings->colorRemoved; } else if (m_diffResult->isAddDifference(resultIndex)) { color = m_settings->colorAdded; } else if (m_diffResult->isPageMoveDifference(resultIndex)) { color = m_settings->colorPageMove; } return color; } void DifferencesDrawInterface::drawPostRendering(QPainter* painter, QRect rect) const { Q_UNUSED(painter); Q_UNUSED(rect); } void DifferencesDrawInterface::drawAnnotations(const pdf::PDFDocument* document, pdf::PDFDocumentBuilder* builder) { pdf::PDFInteger pageCount = document->getCatalog()->getPageCount(); QString title = pdf::PDFSysUtils::getUserName(); QString subject = tr("Difference"); for (pdf::PDFInteger pageIndex = 0; pageIndex < pageCount; ++pageIndex) { const size_t differencesCount = m_diffResult->getDifferencesCount(); const pdf::PDFInteger leftPageIndex = m_mapper->getLeftPageIndex(pageIndex); const pdf::PDFInteger rightPageIndex = m_mapper->getRightPageIndex(pageIndex); const pdf::PDFPage* page = document->getCatalog()->getPage(pageIndex); pdf::PDFObjectReference reference = page->getPageReference(); if (leftPageIndex != -1) { for (size_t i = 0; i < differencesCount; ++i) { auto leftRectangles = m_diffResult->getLeftRectangles(i); for (auto it = leftRectangles.first; it != leftRectangles.second; ++it) { const auto& item = *it; if (item.first == leftPageIndex) { QColor color = getColorForIndex(i); const QRectF& rect = item.second; QPolygonF polygon; polygon << rect.topLeft() << rect.topRight() << rect.bottomRight() << rect.bottomLeft(); pdf::PDFObjectReference annotation = builder->createAnnotationPolygon(reference, polygon, 1.0, color, color, title, subject, m_diffResult->getMessage(i)); builder->setAnnotationOpacity(annotation, 0.3); builder->updateAnnotationAppearanceStreams(annotation); } } } } if (rightPageIndex != -1) { for (size_t i = 0; i < differencesCount; ++i) { auto rightRectangles = m_diffResult->getRightRectangles(i); for (auto it = rightRectangles.first; it != rightRectangles.second; ++it) { const auto& item = *it; if (item.first == rightPageIndex) { QColor color = getColorForIndex(i); const QRectF& rect = item.second; QPolygonF polygon; polygon << rect.topLeft() << rect.topRight() << rect.bottomRight() << rect.bottomLeft(); pdf::PDFObjectReference annotation = builder->createAnnotationPolygon(reference, polygon, 1.0, color, color, title, subject, m_diffResult->getMessage(i)); builder->setAnnotationOpacity(annotation, 0.3); builder->updateAnnotationAppearanceStreams(annotation); } } } } } } void DifferencesDrawInterface::drawRectangle(QPainter* painter, const QTransform& pagePointToDevicePointMatrix, const QRectF& rect, QColor color) const { color.setAlphaF(0.5); QRectF resultRect = pagePointToDevicePointMatrix.mapRect(rect); painter->fillRect(resultRect, color); } void DifferencesDrawInterface::drawMarker(QPainter* painter, const QTransform& pagePointToDevicePointMatrix, const QRectF& rect, QColor color, bool isLeft) const { if (!m_settings->displayMarkers) { return; } pdf::PDFPainterStateGuard guard(painter); QRectF deviceRect = pagePointToDevicePointMatrix.mapRect(rect); QPointF snapPoint; QPointF markPoint; if (isLeft) { snapPoint.ry() = deviceRect.center().y(); snapPoint.rx() = deviceRect.left(); markPoint = snapPoint; markPoint.rx() = 0; } else { snapPoint.ry() = deviceRect.center().y(); snapPoint.rx() = deviceRect.right(); markPoint = snapPoint; markPoint.rx() = painter->device()->width(); } const qreal lineWidthF = pdf::PDFWidgetUtils::scaleDPI_y(painter->device(), 2); QPen pen(Qt::DotLine); pen.setColor(color); pen.setWidthF(lineWidthF); painter->setBrush(Qt::NoBrush); painter->setPen(pen); painter->drawLine(snapPoint, markPoint); const qreal markSizeX = pdf::PDFWidgetUtils::scaleDPI_x(painter->device(), 10); const qreal markSizeY = pdf::PDFWidgetUtils::scaleDPI_y(painter->device(), 10); QPointF ptc = markPoint; QPointF ptTop = ptc; QPointF ptBottom = ptc; ptTop.ry() -= markSizeY; ptBottom.ry() += markSizeY; ptc.rx() += isLeft ? markSizeX : -markSizeX; std::array points = { ptTop, ptc, ptBottom }; painter->setPen(Qt::NoPen); painter->setBrush(QBrush(color)); painter->drawConvexPolygon(points.data(), int(points.size())); } } // namespace pdfdocdiff