diff --git a/resources/etc/OpenBoard.config b/resources/etc/OpenBoard.config index c85c025e..b0665b82 100644 --- a/resources/etc/OpenBoard.config +++ b/resources/etc/OpenBoard.config @@ -123,7 +123,7 @@ RefreshRateInFramePerSecond=2 Margin=20 PageFormat=A4 Resolution=300 -ZoomBehavior=1 +ZoomBehavior=4 [Podcast] AudioRecordingDevice=Default diff --git a/src/adaptors/UBExportPDF.cpp b/src/adaptors/UBExportPDF.cpp index 30834b9a..177f716d 100644 --- a/src/adaptors/UBExportPDF.cpp +++ b/src/adaptors/UBExportPDF.cpp @@ -113,7 +113,7 @@ bool UBExportPDF::persistsDocument(UBDocumentProxy* pDocumentProxy, const QStrin QSize pageSize = scene->sceneSize(); // set high res rendering - scene->setRenderingQuality(UBItem::RenderingQualityHigh); + scene->setRenderingQuality(UBItem::RenderingQualityHigh, UBItem::CacheNotAllowed); scene->setRenderingContext(UBGraphicsScene::NonScreen); // Setting output page size @@ -132,7 +132,7 @@ bool UBExportPDF::persistsDocument(UBDocumentProxy* pDocumentProxy, const QStrin // Restore screen rendering quality scene->setRenderingContext(UBGraphicsScene::Screen); - scene->setRenderingQuality(UBItem::RenderingQualityNormal); + scene->setRenderingQuality(UBItem::RenderingQualityNormal, UBItem::CacheAllowed); // Restore background state scene->setBackground(isDark, pageBackground); diff --git a/src/adaptors/UBThumbnailAdaptor.cpp b/src/adaptors/UBThumbnailAdaptor.cpp index e73973d8..b73f9e57 100644 --- a/src/adaptors/UBThumbnailAdaptor.cpp +++ b/src/adaptors/UBThumbnailAdaptor.cpp @@ -153,12 +153,12 @@ void UBThumbnailAdaptor::persistScene(UBDocumentProxy* proxy, UBGraphicsScene* p } pScene->setRenderingContext(UBGraphicsScene::NonScreen); - pScene->setRenderingQuality(UBItem::RenderingQualityHigh); + pScene->setRenderingQuality(UBItem::RenderingQualityHigh, UBItem::CacheNotAllowed); pScene->render(&painter, imageRect, sceneRect, Qt::KeepAspectRatio); pScene->setRenderingContext(UBGraphicsScene::Screen); - pScene->setRenderingQuality(UBItem::RenderingQualityNormal); + pScene->setRenderingQuality(UBItem::RenderingQualityNormal, UBItem::CacheAllowed); thumb.scaled(width, height, Qt::KeepAspectRatio, Qt::SmoothTransformation).save(fileName, "JPG"); } diff --git a/src/board/UBBoardController.cpp b/src/board/UBBoardController.cpp index ff84aa56..8c172fe0 100644 --- a/src/board/UBBoardController.cpp +++ b/src/board/UBBoardController.cpp @@ -2180,13 +2180,13 @@ void UBBoardController::grabScene(const QRectF& pSceneRect) painter.setRenderHint(QPainter::Antialiasing); mActiveScene->setRenderingContext(UBGraphicsScene::NonScreen); - mActiveScene->setRenderingQuality(UBItem::RenderingQualityHigh); + mActiveScene->setRenderingQuality(UBItem::RenderingQualityHigh, UBItem::CacheNotAllowed); mActiveScene->render(&painter, targetRect, pSceneRect); mActiveScene->setRenderingContext(UBGraphicsScene::Screen); // mActiveScene->setRenderingQuality(UBItem::RenderingQualityNormal); - mActiveScene->setRenderingQuality(UBItem::RenderingQualityHigh); + mActiveScene->setRenderingQuality(UBItem::RenderingQualityHigh, UBItem::CacheAllowed); mPaletteManager->addItem(QPixmap::fromImage(image)); diff --git a/src/core/UBSettings.cpp b/src/core/UBSettings.cpp index b83d5f88..78be3c15 100644 --- a/src/core/UBSettings.cpp +++ b/src/core/UBSettings.cpp @@ -408,7 +408,7 @@ void UBSettings::init() pdfMargin = new UBSetting(this, "PDF", "Margin", "20"); pdfPageFormat = new UBSetting(this, "PDF", "PageFormat", "A4"); pdfResolution = new UBSetting(this, "PDF", "Resolution", "300"); - pdfZoomBehavior = new UBSetting(this, "PDF", "ZoomBehavior", "1"); + pdfZoomBehavior = new UBSetting(this, "PDF", "ZoomBehavior", "4"); podcastFramesPerSecond = new UBSetting(this, "Podcast", "FramesPerSecond", 10); podcastVideoSize = new UBSetting(this, "Podcast", "VideoSize", "Medium"); diff --git a/src/domain/UBGraphicsPDFItem.cpp b/src/domain/UBGraphicsPDFItem.cpp index b9fa19c0..e7161094 100644 --- a/src/domain/UBGraphicsPDFItem.cpp +++ b/src/domain/UBGraphicsPDFItem.cpp @@ -58,6 +58,16 @@ QVariant UBGraphicsPDFItem::itemChange(GraphicsItemChange change, const QVariant return GraphicsPDFItem::itemChange(change, newValue); } +void UBGraphicsPDFItem::updateChild() +{ + CacheMode prevCacheMode = cacheMode(); + + GraphicsPDFItem::update(); + + // Workaround: Necessary, otherwise only the control scene is updated, the display scene refresh is ignored for an unknown reason. + setCacheMode(prevCacheMode); +} + void UBGraphicsPDFItem::setUuid(const QUuid &pUuid) { UBItem::setUuid(pUuid); @@ -142,6 +152,11 @@ void UBGraphicsPDFItem::setRenderingQuality(RenderingQuality pRenderingQuality) } } +void UBGraphicsPDFItem::setCacheBehavior(UBItem::CacheBehavior cacheBehavior) +{ + UBItem::setCacheBehavior(cacheBehavior); + GraphicsPDFItem::setCacheAllowed(cacheBehavior == UBItem::CacheAllowed); +} UBGraphicsScene* UBGraphicsPDFItem::scene() { @@ -153,7 +168,7 @@ UBGraphicsPixmapItem* UBGraphicsPDFItem::toPixmapItem() const { QPixmap pixmap(mRenderer->pageSizeF(mPageNumber).toSize()); QPainter painter(&pixmap); - mRenderer->render(&painter, mPageNumber); + mRenderer->render(&painter, mPageNumber, false /* Cache allowed */); UBGraphicsPixmapItem *pixmapItem = new UBGraphicsPixmapItem(); pixmapItem->setPixmap(pixmap); diff --git a/src/domain/UBGraphicsPDFItem.h b/src/domain/UBGraphicsPDFItem.h index 3107cb2d..3d234c94 100644 --- a/src/domain/UBGraphicsPDFItem.h +++ b/src/domain/UBGraphicsPDFItem.h @@ -59,13 +59,14 @@ class UBGraphicsPDFItem: public GraphicsPDFItem, public UBItem, public UBGraphic virtual void setRenderingQuality(RenderingQuality pRenderingQuality); + virtual void setCacheBehavior(CacheBehavior cacheBehavior); + virtual UBGraphicsScene* scene(); virtual UBGraphicsPixmapItem* toPixmapItem() const; virtual void clearSource(){;} virtual void setUuid(const QUuid &pUuid); - protected: virtual void mousePressEvent(QGraphicsSceneMouseEvent *event); @@ -74,7 +75,9 @@ class UBGraphicsPDFItem: public GraphicsPDFItem, public UBItem, public UBGraphic virtual void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget); virtual QVariant itemChange(GraphicsItemChange change, const QVariant &value); - + virtual void updateChild(); + private slots: + void OnRequireUpdate(); }; #endif /* UBGRAPHICSPDFITEM_H_ */ diff --git a/src/domain/UBGraphicsScene.cpp b/src/domain/UBGraphicsScene.cpp index f07e31ec..4dd7725a 100644 --- a/src/domain/UBGraphicsScene.cpp +++ b/src/domain/UBGraphicsScene.cpp @@ -2396,7 +2396,7 @@ void UBGraphicsScene::addMask(const QPointF ¢er) curtain->setSelected(true); } -void UBGraphicsScene::setRenderingQuality(UBItem::RenderingQuality pRenderingQuality) +void UBGraphicsScene::setRenderingQuality(UBItem::RenderingQuality pRenderingQuality, UBItem::CacheBehavior cacheBehavior) { QListIterator itItems(mFastAccessItems); @@ -2409,6 +2409,7 @@ void UBGraphicsScene::setRenderingQuality(UBItem::RenderingQuality pRenderingQua if (ubItem) { ubItem->setRenderingQuality(pRenderingQuality); + ubItem->setCacheBehavior(cacheBehavior); } } } diff --git a/src/domain/UBGraphicsScene.h b/src/domain/UBGraphicsScene.h index 3c6d6751..4040af7e 100644 --- a/src/domain/UBGraphicsScene.h +++ b/src/domain/UBGraphicsScene.h @@ -295,7 +295,7 @@ class UBGraphicsScene: public UBCoreGraphicsScene, public UBItem mViewState = pViewState; } - virtual void setRenderingQuality(UBItem::RenderingQuality pRenderingQuality); + virtual void setRenderingQuality(UBItem::RenderingQuality pRenderingQuality, UBItem::CacheBehavior cacheBehavior); QList relativeDependencies() const; diff --git a/src/domain/UBItem.h b/src/domain/UBItem.h index 063724f7..43629949 100644 --- a/src/domain/UBItem.h +++ b/src/domain/UBItem.h @@ -51,6 +51,13 @@ class UBItem RenderingQualityNormal = 0, RenderingQualityHigh }; + enum CacheBehavior + { + CacheNotAllowed, + CacheAllowed, + nbrCacheBehavior + }; + virtual QUuid uuid() const { return mUuid; @@ -71,6 +78,11 @@ class UBItem mRenderingQuality = pRenderingQuality; } + virtual void setCacheBehavior(CacheBehavior cacheBehavior) + { + mCacheBehavior = cacheBehavior; + } + virtual UBItem* deepCopy() const = 0; virtual void copyItemParameters(UBItem *copy) const = 0; @@ -98,6 +110,7 @@ class UBItem QUrl mSourceUrl; + CacheBehavior mCacheBehavior; }; class UBGraphicsItem diff --git a/src/pdf/GraphicsPDFItem.cpp b/src/pdf/GraphicsPDFItem.cpp index 090d1142..110ad015 100644 --- a/src/pdf/GraphicsPDFItem.cpp +++ b/src/pdf/GraphicsPDFItem.cpp @@ -39,13 +39,16 @@ GraphicsPDFItem::GraphicsPDFItem(PDFRenderer *renderer, int pageNumber, QGraphic : QObject(0), QGraphicsItem(parentItem) , mRenderer(renderer) , mPageNumber(pageNumber) + , mIsCacheAllowed(true) { setCacheMode(QGraphicsItem::DeviceCoordinateCache); mRenderer->attach(); + connect(mRenderer, SIGNAL(signalUpdateParent()), this, SLOT(OnRequireUpdate())); } GraphicsPDFItem::~GraphicsPDFItem() { + disconnect(mRenderer, SIGNAL(signalUpdateParent()), this, SLOT(OnRequireUpdate())); mRenderer->detach(); } @@ -70,8 +73,15 @@ void GraphicsPDFItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *o } if (option) - mRenderer->render(painter, mPageNumber, option->exposedRect); - else + { + mRenderer->render(painter, mPageNumber, mIsCacheAllowed, option->exposedRect); + } else qWarning("GraphicsPDFItem::paint: option is null, ignoring painting"); } + +void GraphicsPDFItem::OnRequireUpdate() +{ + updateChild(); +} + diff --git a/src/pdf/GraphicsPDFItem.h b/src/pdf/GraphicsPDFItem.h index a8cb91f2..5ec60808 100644 --- a/src/pdf/GraphicsPDFItem.h +++ b/src/pdf/GraphicsPDFItem.h @@ -51,10 +51,15 @@ class GraphicsPDFItem : public QObject, public QGraphicsItem int pageNumber() const { return mPageNumber; } QUuid fileUuid() const { return mRenderer->fileUuid(); } QByteArray fileData() const { return mRenderer->fileData(); } - + void setCacheAllowed(bool const value) { mIsCacheAllowed = value; } + virtual void updateChild() = 0; protected: PDFRenderer *mRenderer; int mPageNumber; + bool mIsCacheAllowed; + + private slots: + void OnRequireUpdate(); }; #endif // GRAPHICSPDFITEM_H diff --git a/src/pdf/PDFRenderer.h b/src/pdf/PDFRenderer.h index 994a7b09..8dd3f298 100644 --- a/src/pdf/PDFRenderer.h +++ b/src/pdf/PDFRenderer.h @@ -66,8 +66,7 @@ class PDFRenderer : public QObject void setDPI(int desiredDPI) { this->dpiForRendering = desiredDPI; } - public slots: - virtual void render(QPainter *p, int pageNumber, const QRectF &bounds = QRectF()) = 0; + virtual void render(QPainter *p, int pageNumber, bool const cacheAllowed, const QRectF &bounds = QRectF()) = 0; private: QAtomicInt mRefCount; diff --git a/src/pdf/UBWebPluginPDFWidget.cpp b/src/pdf/UBWebPluginPDFWidget.cpp index b7d71150..98c10655 100644 --- a/src/pdf/UBWebPluginPDFWidget.cpp +++ b/src/pdf/UBWebPluginPDFWidget.cpp @@ -152,7 +152,9 @@ void UBWebPluginPDFWidget::paintEvent(QPaintEvent *event) painter.translate((geometry().width() - (pageSize.width() * mScale)) / 2, 0); painter.scale(mScale, mScale); - mRenderer->render(&painter, mPageNumber, event->rect()); + // Note: If you ever want to use the cache, you need to make sure the object 'update' is called + // when the processing is completed. See 'signalUpdateParent'. + mRenderer->render(&painter, mPageNumber, false /* Cache allowed */, event->rect()); painter.setPen(QPen(Qt::gray, 1)); painter.drawRect(0, 0, pageSize.width(), pageSize.height()); } diff --git a/src/pdf/XPDFRenderer.cpp b/src/pdf/XPDFRenderer.cpp index e128b33b..301406c5 100644 --- a/src/pdf/XPDFRenderer.cpp +++ b/src/pdf/XPDFRenderer.cpp @@ -42,21 +42,43 @@ QAtomicInt XPDFRenderer::sInstancesCount = 0; -XPDFRenderer::XPDFRenderer(const QString &filename, bool importingFile) : - mpSplashBitmapHistorical(nullptr), mSplashHistorical(nullptr), mDocument(nullptr) +namespace constants{ + SplashColor paperColor = {0xFF, 0xFF, 0xFF}; // white +} + +XPDFRenderer::XPDFRenderer(const QString &filename, bool importingFile) + : m_pdfZoomMode(UBSettings::settings()->pdfZoomBehavior->get().toUInt()) + , mpSplashBitmapHistorical(nullptr) + , mSplashHistorical(nullptr) + , mDocument(nullptr) { - switch (UBSettings::settings()->pdfZoomBehavior->get().toUInt()) { + switch (m_pdfZoomMode) { case 0: // Render each time (historical initial implementation). break; case 1: // Render a single image, degradated quality when zoomed big. default: m_pdfZoomCache.push_back(XPDFRendererZoomFactor::mode1_zoomFactor); break; - case 2: // Render three images, optimal quality all the time. + case 2: // Render three images, use downsampling, optimal quality all the time, slower. m_pdfZoomCache.push_back(XPDFRendererZoomFactor::mode2_zoomFactorStage1); m_pdfZoomCache.push_back(XPDFRendererZoomFactor::mode2_zoomFactorStage2); m_pdfZoomCache.push_back(XPDFRendererZoomFactor::mode2_zoomFactorStage3); break; + case 3: // Do not downsample, minimal loss, faster. Not necessarily the expected result, + // because a 'zoom factor 1' here does not correspond to a user choice 'zoom factor 1'. + // The zoom requested is dependent on many factors, including the input pdf, the output screen resolution + // and the zoom user choice. Thus, the 'mode3_zoomFactorStage1' might be fine on one screen, but + // fuzzy on another one. + m_pdfZoomCache.push_back(XPDFRendererZoomFactor::mode3_zoomFactorStage1); + m_pdfZoomCache.push_back(XPDFRendererZoomFactor::mode3_zoomFactorStage2); + break; + case 4: // Multithreaded, several steps, downsampled. + for (int i = 0; i < XPDFRendererZoomFactor::mode4_zoomFactorIterations; i++ ) + { + double const zoomValue = XPDFRendererZoomFactor::mode4_zoomFactorStart+XPDFRendererZoomFactor::mode4_zoomFactorStepSquare*static_cast(i*i); + m_pdfZoomCache.push_back(zoomValue); + } + break; } Q_UNUSED(importingFile); @@ -77,18 +99,19 @@ XPDFRenderer::XPDFRenderer(const QString &filename, bool importingFile) : mDocument = new PDFDoc(new GooString(filename.toLocal8Bit()), 0, 0, 0); // the filename GString is deleted on PDFDoc desctruction #endif sInstancesCount.ref(); + connect(&m_cacheThread, SIGNAL(finished()), this, SLOT(OnThreadFinished())); } XPDFRenderer::~XPDFRenderer() { + disconnect(&m_cacheThread, SIGNAL(finished()), this, SLOT(OnThreadFinished())); + m_cacheThread.cancelPending(); + m_cacheThread.wait(); // Would crash if data deleted during processing. + for(int i = 0; i < m_pdfZoomCache.size(); i++) { PdfZoomCacheData &cacheData = m_pdfZoomCache[i]; - if(cacheData.splash != nullptr){ - cacheData.cachedImage = QImage(); // The 'cachedImage' uses a buffer from 'splash'. - delete cacheData.splash; - cacheData.splash = nullptr; - } + cacheData.cleanup(); } if(mSplashHistorical) @@ -202,10 +225,10 @@ QImage* XPDFRenderer::createPDFImageHistorical(int pageNumber, qreal xscale, qre { if (isValid()) { - SplashColor paperColor = {0xFF, 0xFF, 0xFF}; // white if(mSplashHistorical) delete mSplashHistorical; - mSplashHistorical = new SplashOutputDev(splashModeRGB8, 1, false, paperColor); + + mSplashHistorical = new SplashOutputDev(splashModeRGB8, 1, false, constants::paperColor); #ifdef USE_XPDF mSplashHistorical->startDoc(mDocument->getXRef()); #else @@ -239,41 +262,120 @@ QImage* XPDFRenderer::createPDFImageHistorical(int pageNumber, qreal xscale, qre return new QImage(mpSplashBitmapHistorical->getDataPtr(), mpSplashBitmapHistorical->getWidth(), mpSplashBitmapHistorical->getHeight(), mpSplashBitmapHistorical->getWidth() * 3, QImage::Format_RGB888); } -void XPDFRenderer::render(QPainter *p, int pageNumber, const QRectF &bounds) +void XPDFRenderer::OnThreadFinished() +{ + emit signalUpdateParent(); + if (m_cacheThread.isJobPending()) + m_cacheThread.start(); +} + +void XPDFRenderer::render(QPainter *p, int pageNumber, bool const cacheAllowed, const QRectF &bounds) { + //qDebug() << "render enter"; Q_UNUSED(bounds); if (isValid()) { - if (m_pdfZoomCache.size() > 0) + if (m_pdfZoomCache.size() > 0 && cacheAllowed) { qreal xscale = p->worldTransform().m11(); qreal yscale = p->worldTransform().m22(); Q_ASSERT(qFuzzyCompare(xscale, yscale)); // Zoom equal in all axes expected. Q_ASSERT(xscale > 0.0); // Potential Div0 later if this assert fail. + qreal zoomRequested = xscale; int zoomIndex = 0; - bool foundIndex = false; - for (; zoomIndex < m_pdfZoomCache.size() && !foundIndex;) + if (m_pdfZoomMode == 3) { - if (xscale <= m_pdfZoomCache[zoomIndex].ratio) { - foundIndex = true; - } else { - zoomIndex++; + // Choose a zoom which is inferior or equivalent than the user choice (= minor loss, downscaling). + bool foundIndex = false; + for (zoomIndex = m_pdfZoomCache.size()-1; zoomIndex >= 0 && !foundIndex;) + { + if (zoomRequested >= m_pdfZoomCache[zoomIndex].ratio) { + foundIndex = true; + } else { + zoomIndex--; + } } + + if (!foundIndex) // Use the smallest one. + zoomIndex = 0; + + if (zoomIndex == 0 && m_pdfZoomCache[zoomIndex].ratio != zoomRequested) + { + m_pdfZoomCache[zoomIndex].cleanup(); + m_pdfZoomCache[zoomIndex] = PdfZoomCacheData(zoomRequested); + } + } else { + // Choose a zoom which is superior or equivalent than the user choice (= no loss, upscaling). + bool foundIndex = false; + for (; zoomIndex < m_pdfZoomCache.size() && !foundIndex;) + { + if (zoomRequested <= (m_pdfZoomCache[zoomIndex].ratio+0.1)) { + foundIndex = true; + } else { + zoomIndex++; + } + } + + if (!foundIndex) // Use the previous one. + zoomIndex--; } - if (!foundIndex) // Use the previous one. - zoomIndex--; + QImage pdfImage = createPDFImageCached(pageNumber, m_pdfZoomCache[zoomIndex]); + qreal ratioExpected = m_pdfZoomCache[zoomIndex].ratio; + qreal ratioObtained = ratioExpected; + int const initialZoomIndex = zoomIndex; + + if (pdfImage == QImage() && m_pdfZoomCache[zoomIndex].hasToBeProcessed) + { + // Try to temporarily fallback on a valid image, for a fuzzy or downsampled preview. + // The actual result will be updated after the processing. + bool isCurrent = true; + while (zoomIndex < m_pdfZoomCache.size()-1 && (m_pdfZoomCache[zoomIndex].cachedImage == QImage() || (m_pdfZoomCache[zoomIndex].cachedPageNumber != pageNumber && !isCurrent))) + { + zoomIndex = zoomIndex+1; + isCurrent = false; + } + while (zoomIndex > 0 && (m_pdfZoomCache[zoomIndex].cachedImage == QImage() || m_pdfZoomCache[zoomIndex].cachedPageNumber != pageNumber)) + zoomIndex = zoomIndex-1; + ratioObtained = m_pdfZoomCache[zoomIndex].ratio; + } + if (m_pdfZoomCache[zoomIndex].cachedImage == QImage() || m_pdfZoomCache[zoomIndex].cachedPageNumber != pageNumber) + { + // No alternate image found. Build an alternate image in order to display some progress. + // Also make sure we fallback to the initial ratio request. + zoomIndex = initialZoomIndex; + qreal ratioDiff = m_pdfZoomCache[zoomIndex].ratio; + pdfImage = QImage(bounds.width()*ratioDiff, bounds.height()*ratioDiff, QImage::Format_RGB888); + pdfImage.fill("white"); + + QPainter painter(&pdfImage); + QString const text = tr("Processing..."); + QFont font = painter.font(); + if (font.pixelSize() != -1) + font.setPixelSize(ratioDiff*font.pixelSize()); + else + font.setPointSizeF(ratioDiff*font.pointSizeF()); + painter.setFont(font); + QFontMetrics textMetric(font, &pdfImage); + QSize textSize = textMetric.size(0, text); + painter.drawText((bounds.width()*ratioDiff-textSize.width())/2, (bounds.height()*ratioDiff-textSize.height())/2, text); + } else { + pdfImage = m_pdfZoomCache[zoomIndex].cachedImage; + } - QImage const &pdfImage = createPDFImageCached(pageNumber, m_pdfZoomCache[zoomIndex]); QTransform savedTransform = p->worldTransform(); double const ratioDifferenceBetweenWorldAndImage = 1.0/m_pdfZoomCache[zoomIndex].ratio; - // The 'pdfImage' is rendered with a quality equal or superior. We adjust the 'transform' to zoom it - // out the required ratio. + // The 'pdfImage' is maybe rendered with a different quality than requested. We adjust the 'transform' to zoom it + // in or out of the required ratio. QTransform newTransform = savedTransform.scale(ratioDifferenceBetweenWorldAndImage, ratioDifferenceBetweenWorldAndImage); p->setWorldTransform(newTransform); + /* qDebug() << "drawImage size=" << p->viewport() << "bounds" << bounds << + "pdfImage" << pdfImage.size() << "savedTransform" << savedTransform.m11() << + "ratioDiff" << ratioDifferenceBetweenWorldAndImage << "zoomRequested" << zoomRequested << + "zoomIndex" << zoomIndex; */ p->drawImage(QPointF( mSliceX, mSliceY), pdfImage); p->setWorldTransform(savedTransform); @@ -284,46 +386,89 @@ void XPDFRenderer::render(QPainter *p, int pageNumber, const QRectF &bounds) QImage *pdfImage = createPDFImageHistorical(pageNumber, xscale, yscale, bounds); QTransform savedTransform = p->worldTransform(); p->resetTransform(); + //qDebug() << "drawImage size=" << p->viewport() << "bounds" << bounds << "pdfImage" << pdfImage->size() << "savedTransform" << savedTransform.m11(); p->drawImage(QPointF(savedTransform.dx() + mSliceX, savedTransform.dy() + mSliceY), *pdfImage); p->setWorldTransform(savedTransform); delete pdfImage; } } + //qDebug() << "render leave"; } QImage& XPDFRenderer::createPDFImageCached(int pageNumber, PdfZoomCacheData &cacheData) { if (isValid()) { - SplashColor paperColor = {0xFF, 0xFF, 0xFF}; // white - if (cacheData.requireUpdateImage(pageNumber)) - { - cacheData.prepareNewSplash(pageNumber, paperColor); - -#ifdef USE_XPDF - cacheData.splash->startDoc(mDocument->getXRef()); -#else - cacheData.splash->startDoc(mDocument); -#endif - int rotation = 0; // in degrees (get it from the worldTransform if we want to support rotation) - bool useMediaBox = false; - bool crop = true; - bool printing = false; + if (cacheData.requireUpdateImage(pageNumber) && !cacheData.hasToBeProcessed) + { mSliceX = 0.; mSliceY = 0.; - mDocument->displayPage(cacheData.splash, pageNumber, this->dpiForRendering * cacheData.ratio, this->dpiForRendering * cacheData.ratio, - rotation, useMediaBox, crop, printing); - cacheData.splashBitmap = cacheData.splash->getBitmap(); + CacheThread::JobData jobData; + jobData.cacheData = &cacheData; + jobData.document = mDocument; + jobData.dpiForRendering = this->dpiForRendering; + jobData.pageNumber = pageNumber; + jobData.cacheData->hasToBeProcessed = true; + // Make sure we reset that image, because the data uses 'splash' buffer, which will be deallocated and + // reallocated when the job is started. + jobData.cacheData->cachedImage = QImage(); + m_cacheThread.pushJob(jobData); + + if (m_pdfZoomMode == 4) + { + // Start the job multithreaded. The item will be refreshed when the signal 'finished' is emitted. + m_cacheThread.start(); + } else { + // Perform the job now. Note this will lock the GUI until the job is done. + m_cacheThread.run(); + } } - - // Note this uses the 'cacheData.splash->getBitmap()->getDataPtr()' as data buffer. - cacheData.cachedImage = QImage(cacheData.splashBitmap->getDataPtr(), cacheData.splashBitmap->getWidth(), cacheData.splashBitmap->getHeight(), - cacheData.splashBitmap->getWidth() * 3 /* bytesPerLine, 24 bits for RGB888, = 3 bytes */, - QImage::Format_RGB888); } else { cacheData.cachedImage = QImage(); } return cacheData.cachedImage; } + +void XPDFRenderer::CacheThread::run() +{ + m_jobMutex.lock(); + + CacheThread::JobData jobData = m_nextJob.first(); + m_nextJob.pop_front(); + /* qDebug() << "XPDFRenderer::CacheThread starting page" << jobData.pageNumber + << "ratio" << jobData.cacheData->ratio; */ + + jobData.cacheData->prepareNewSplash(jobData.pageNumber, constants::paperColor); + +#ifdef USE_XPDF + jobData.cacheData->splash->startDoc(jobData.document->getXRef()); +#else + jobData.cacheData->splash->startDoc(jobData.document); +#endif + + m_jobMutex.unlock(); + + int rotation = 0; // in degrees (get it from the worldTransform if we want to support rotation) + bool useMediaBox = false; + bool crop = true; + bool printing = false; + + jobData.document->displayPage(jobData.cacheData->splash, jobData.pageNumber, jobData.dpiForRendering * jobData.cacheData->ratio, + jobData.dpiForRendering * jobData.cacheData->ratio, + rotation, useMediaBox, crop, printing); + + m_jobMutex.lock(); + jobData.cacheData->splashBitmap = jobData.cacheData->splash->getBitmap(); + // Note this uses the 'cacheData.splash->getBitmap()->getDataPtr()' as data buffer. + jobData.cacheData->cachedImage = QImage(jobData.cacheData->splashBitmap->getDataPtr(), jobData.cacheData->splashBitmap->getWidth(), jobData.cacheData->splashBitmap->getHeight(), + jobData.cacheData->splashBitmap->getWidth() * 3 /* bytesPerLine, 24 bits for RGB888, = 3 bytes */, + QImage::Format_RGB888); + + /* qDebug() << "XPDFRenderer::CacheThread completed page" << jobData.pageNumber + << "ratio" << jobData.cacheData->ratio << "final size is" << jobData.cacheData->cachedImage.size(); */ + + jobData.cacheData->hasToBeProcessed = false; + m_jobMutex.unlock(); +} diff --git a/src/pdf/XPDFRenderer.h b/src/pdf/XPDFRenderer.h index 4ac3f9aa..bd31982b 100644 --- a/src/pdf/XPDFRenderer.h +++ b/src/pdf/XPDFRenderer.h @@ -30,6 +30,8 @@ #ifndef XPDFRENDERER_H #define XPDFRENDERER_H #include +#include +#include #include "PDFRenderer.h" #include @@ -58,6 +60,11 @@ namespace XPDFRendererZoomFactor const double mode2_zoomFactorStage1 = 2.5; const double mode2_zoomFactorStage2 = 5.0; const double mode2_zoomFactorStage3 = 10.0; + const double mode3_zoomFactorStage1 = 1.0; + const double mode3_zoomFactorStage2 = 3.0; + const double mode4_zoomFactorStart = .25; + const double mode4_zoomFactorStepSquare = .25; + const double mode4_zoomFactorIterations = 7; } class XPDFRenderer : public PDFRenderer @@ -68,30 +75,30 @@ class XPDFRenderer : public PDFRenderer XPDFRenderer(const QString &filename, bool importingFile = false); virtual ~XPDFRenderer(); - bool isValid() const; + virtual bool isValid() const override; + virtual int pageCount() const override; + virtual QSizeF pageSizeF(int pageNumber) const override; + virtual int pageRotation(int pageNumber) const override; + virtual QString title() const override; + virtual void render(QPainter *p, int pageNumber, const bool cacheAllowed, const QRectF &bounds = QRectF()) override; - virtual int pageCount() const; - - virtual QSizeF pageSizeF(int pageNumber) const; - - virtual int pageRotation(int pageNumber) const; - - virtual QString title() const; - - public slots: - void render(QPainter *p, int pageNumber, const QRectF &bounds = QRectF()); + signals: + void signalUpdateParent(); private: void init(); struct PdfZoomCacheData { - PdfZoomCacheData(double const a_ratio) : splashBitmap(nullptr), cachedPageNumber(-1), splash(nullptr), ratio(a_ratio) {}; + PdfZoomCacheData(double const a_ratio) : splashBitmap(nullptr), cachedPageNumber(-1), splash(nullptr), ratio(a_ratio), hasToBeProcessed(false) {}; ~PdfZoomCacheData() {}; SplashBitmap* splashBitmap; + //! Note: The 'cachedImage' uses a buffer from 'splash'. Make sure it is invalidated BEFORE 'splash' deallocation. QImage cachedImage; int cachedPageNumber; SplashOutputDev* splash; - double const ratio; + double ratio; + bool hasToBeProcessed; + QList updateListAfterProcessing; bool requireUpdateImage(int const pageNumber) const { return (pageNumber != cachedPageNumber) || (splash == nullptr); @@ -107,15 +114,55 @@ class XPDFRenderer : public PDFRenderer splash = new SplashOutputDev(splashModeRGB8, 1, false, paperColor); cachedPageNumber = pageNumber; } + + void cleanup() + { + if(splash != nullptr){ + cachedImage = QImage(); + delete splash; + splash = nullptr; + } + } + }; + + //! Spawned when a pdf processing is required, when no matching image is found in cache. + class CacheThread : public QThread + { + public: + struct JobData { + PdfZoomCacheData* cacheData; + PDFDoc *document; + int pageNumber; + double dpiForRendering; + }; + + CacheThread() {} + ~CacheThread() {} + void pushJob(JobData &jobData) { + QMutexLocker lock(&m_jobMutex); + m_nextJob.push_back(jobData); + } + + virtual void run() override; + bool isJobPending() { QMutexLocker lock(&m_jobMutex); return m_nextJob.size() > 0; } + void cancelPending() { QMutexLocker lock(&m_jobMutex); m_nextJob.clear(); } + private: + QList m_nextJob; + QMutex m_jobMutex; }; + CacheThread m_cacheThread; + QImage &createPDFImageCached(int pageNumber, PdfZoomCacheData &cacheData); QImage* createPDFImageHistorical(int pageNumber, qreal xscale, qreal yscale, const QRectF &bounds); - // Used when 'ZoomBehavior == 1 or 2'. + // Used when 'ZoomBehavior == 1, 2, 3 or 4'. // =1 has only x3 zoom in cache (= loss if user zoom > 3.0). // =2, has 2.5, 5 and 10 (= no loss, but a bit slower). + // =3, has 1.0, 2.5, 5 and 10, but downsampled instead of upsampled (= minor quality loss, a bit faster). + // =4, multithreaded, multiple level of zoom. QVector m_pdfZoomCache; + int const m_pdfZoomMode; // Used when 'ZoomBehavior == 0' (no cache). SplashBitmap* mpSplashBitmapHistorical; @@ -126,6 +173,9 @@ class XPDFRenderer : public PDFRenderer static QAtomicInt sInstancesCount; qreal mSliceX; qreal mSliceY; + +private slots: + void OnThreadFinished(); }; #endif // XPDFRENDERER_H