Rendering pdf to images (first part)

This commit is contained in:
Jakub Melka 2020-02-09 16:06:29 +01:00
parent a1d270e2ab
commit 15805d80af
10 changed files with 510 additions and 18 deletions

View File

@ -393,7 +393,8 @@ PDFDrawWidgetProxy::PDFDrawWidgetProxy(QObject* parent) :
m_compiler(new PDFAsynchronousPageCompiler(this)),
m_textLayoutCompiler(new PDFAsynchronousTextLayoutCompiler(this)),
m_rasterizer(new PDFRasterizer(this)),
m_progress(nullptr)
m_progress(nullptr),
m_useOpenGL(false)
{
m_controller = new PDFDrawSpaceController(this);
connect(m_controller, &PDFDrawSpaceController::drawSpaceChanged, this, &PDFDrawWidgetProxy::update);
@ -1117,6 +1118,8 @@ bool PDFDrawWidgetProxy::isBlockMode() const
void PDFDrawWidgetProxy::updateRenderer(bool useOpenGL, const QSurfaceFormat& surfaceFormat)
{
m_useOpenGL = useOpenGL;
m_surfaceFormat = surfaceFormat;
m_rasterizer->reset(useOpenGL, surfaceFormat);
}

View File

@ -320,6 +320,8 @@ public:
void setProgress(PDFProgress* progress) { m_progress = progress; }
PDFAsynchronousTextLayoutCompiler* getTextLayoutCompiler() const { return m_textLayoutCompiler; }
PDFWidget* getWidget() const { return m_widget; }
bool isUsingOpenGL() const { return m_useOpenGL; }
const QSurfaceFormat& getSurfaceFormat() const { return m_surfaceFormat; }
void setFeatures(PDFRenderer::Features features);
void setPreferredMeshResolutionRatio(PDFReal ratio);
@ -470,6 +472,12 @@ private:
/// Additional drawing interfaces
std::set<IDocumentDrawInterface*> m_drawInterfaces;
/// Use OpenGL for rendering?
bool m_useOpenGL;
/// Surface format for OpenGL
QSurfaceFormat m_surfaceFormat;
};
} // namespace pdf

View File

@ -44,7 +44,8 @@ enum RenderErrorType
Error,
Warning,
NotImplemented,
NotSupported
NotSupported,
Information
};
struct PDFRenderError

View File

@ -18,7 +18,10 @@
#include "pdfrenderer.h"
#include "pdfpainter.h"
#include "pdfdocument.h"
#include "pdfexecutionpolicy.h"
#include "pdfprogress.h"
#include <QDir>
#include <QElapsedTimer>
#include <QOpenGLContext>
#include <QOffscreenSurface>
@ -336,10 +339,120 @@ void PDFRasterizer::releaseOpenGL()
}
}
PDFRasterizer* PDFRasterizerPool::acquire()
{
m_semaphore.acquire();
QMutexLocker guard(&m_mutex);
Q_ASSERT(!m_rasterizers.empty());
PDFRasterizer* rasterizer = m_rasterizers.back();
m_rasterizers.pop_back();
return rasterizer;
}
void PDFRasterizerPool::release(pdf::PDFRasterizer* rasterizer)
{
QMutexLocker guard(&m_mutex);
Q_ASSERT(std::find(m_rasterizers.cbegin(), m_rasterizers.cend(), rasterizer) == m_rasterizers.cend());
m_rasterizers.push_back(rasterizer);
// Jakub Melka: we must release it at the end, to ensure rasterizer is in the array before
// semaphore is released, to avoid race condition.
m_semaphore.release();
}
void PDFRasterizerPool::render(const std::vector<PDFInteger>& pageIndices,
const PDFRasterizerPool::PageImageSizeGetter& imageSizeGetter,
const PDFRasterizerPool::ProcessImageMethod& processImage,
PDFProgress* progress)
{
if (pageIndices.empty())
{
return;
}
Q_ASSERT(imageSizeGetter);
Q_ASSERT(processImage);
QElapsedTimer timer;
timer.start();
emit renderError(PDFRenderError(RenderErrorType::Information, PDFTranslationContext::tr("Start at %1...").arg(QTime::currentTime().toString(Qt::TextDate))));
if (progress)
{
ProgressStartupInfo info;
info.showDialog = true;
info.text = PDFTranslationContext::tr("Rendering document into images.");
progress->start(pageIndices.size(), qMove(info));
}
auto processPage = [this, progress, &imageSizeGetter, &processImage](const PDFInteger pageIndex)
{
const PDFPage* page = m_document->getCatalog()->getPage(pageIndex);
if (!page)
{
if (progress)
{
progress->step();
}
emit renderError(PDFRenderError(RenderErrorType::Error, PDFTranslationContext::tr("Page %1 not found.").arg(pageIndex)));
return;
}
// Precompile the page
PDFPrecompiledPage precompiledPage;
PDFRenderer renderer(m_document, m_fontCache, m_cms, m_optionalContentActivity, m_features, m_meshQualitySettings);
renderer.compile(&precompiledPage, pageIndex);
for (const PDFRenderError error : precompiledPage.getErrors())
{
emit renderError(PDFRenderError(error.type, PDFTranslationContext::tr("Page %1: %2").arg(pageIndex + 1).arg(error.message)));
}
// Render page to image
PDFRasterizer* rasterizer = acquire();
QImage image = rasterizer->render(page, &precompiledPage, imageSizeGetter(page), m_features);
release(rasterizer);
// Now, process the image
processImage(pageIndex, qMove(image));
if (progress)
{
progress->step();
}
};
PDFExecutionPolicy::execute(PDFExecutionPolicy::Scope::Page, pageIndices.cbegin(), pageIndices.cend(), processPage);
if (progress)
{
progress->finish();
}
emit renderError(PDFRenderError(RenderErrorType::Information, PDFTranslationContext::tr("Finished at %1...").arg(QTime::currentTime().toString(Qt::TextDate))));
emit renderError(PDFRenderError(RenderErrorType::Information, PDFTranslationContext::tr("%1 miliseconds elapsed to render %2 pages...").arg(timer.nsecsElapsed() / 1000000).arg(pageIndices.size())));
}
int PDFRasterizerPool::getDefaultRasterizerCount()
{
int hint = QThread::idealThreadCount() / 2;
return qBound(1, hint, 16);
}
PDFImageWriterSettings::PDFImageWriterSettings()
{
m_formats = QImageWriter::supportedImageFormats();
selectFormat(m_formats.front());
constexpr const char* DEFAULT_FORMAT = "png";
if (m_formats.count(DEFAULT_FORMAT))
{
selectFormat(DEFAULT_FORMAT);
}
else
{
selectFormat(m_formats.front());
}
}
void PDFImageWriterSettings::selectFormat(const QByteArray& format)
@ -460,7 +573,8 @@ void PDFImageWriterSettings::setCurrentSubtype(const QByteArray& currentSubtype)
m_currentSubtype = currentSubtype;
}
PDFPageImageExportSettings::PDFPageImageExportSettings()
PDFPageImageExportSettings::PDFPageImageExportSettings(const PDFDocument* document) :
m_document(document)
{
m_fileTemplate = PDFTranslationContext::tr("Image_%");
}
@ -535,4 +649,174 @@ void PDFPageImageExportSettings::setPixelResolution(int pixelResolution)
m_pixelResolution = pixelResolution;
}
bool PDFPageImageExportSettings::validate(QString* errorMessagePtr)
{
QString dummy;
QString& errorMessage = errorMessagePtr ? *errorMessagePtr : dummy;
if (m_directory.isEmpty())
{
errorMessage = PDFTranslationContext::tr("Target directory is empty.");
return false;
}
// Check, if target directory exists
QDir directory(m_directory);
if (!directory.exists())
{
errorMessage = PDFTranslationContext::tr("Target directory '%1' doesn't exist.").arg(m_directory);
return false;
}
if (m_fileTemplate.isEmpty())
{
errorMessage = PDFTranslationContext::tr("File template is empty.");
return false;
}
if (!m_fileTemplate.contains("%"))
{
errorMessage = PDFTranslationContext::tr("File template must contain character '%' for page number.");
return false;
}
// Check page selection
if (m_pageSelectionMode == PageSelectionMode::Selection)
{
std::vector<PDFInteger> pages = getPages();
if (pages.empty())
{
errorMessage = PDFTranslationContext::tr("Page list is invalid. It should have form such as '1-12,17,24,27-29'.");
return false;
}
if (pages.back() >= PDFInteger(m_document->getCatalog()->getPageCount()))
{
errorMessage = PDFTranslationContext::tr("Page list contains page, which is not in the document (%1).").arg(pages.back());
return false;
}
}
if (m_resolutionMode == ResolutionMode::DPI && (m_dpiResolution < getMinDPIResolution() || m_dpiResolution > getMaxDPIResolution()))
{
errorMessage = PDFTranslationContext::tr("DPI resolution should be in range %1 to %2.").arg(getMinDPIResolution()).arg(getMaxDPIResolution());
return false;
}
if (m_resolutionMode == ResolutionMode::Pixels && (m_pixelResolution < getMinPixelResolution() || m_pixelResolution > getMaxPixelResolution()))
{
errorMessage = PDFTranslationContext::tr("Pixel resolution should be in range %1 to %2.").arg(getMinPixelResolution()).arg(getMaxPixelResolution());
return false;
}
return true;
}
std::vector<PDFInteger> PDFPageImageExportSettings::getPages() const
{
std::vector<PDFInteger> result;
switch (m_pageSelectionMode)
{
case PageSelectionMode::All:
{
result.resize(m_document->getCatalog()->getPageCount(), 0);
std::iota(result.begin(), result.end(), 0);
break;
}
case PageSelectionMode::Selection:
{
bool ok = false;
QStringList parts = m_pageSelection.split(QChar(','), Qt::SkipEmptyParts, Qt::CaseSensitive);
for (const QString& part : parts)
{
QStringList numbers = part.split(QChar('-'), Qt::KeepEmptyParts, Qt::CaseSensitive);
switch (numbers.size())
{
case 1:
{
const QString& numberString = numbers.front();
result.push_back(numberString.toLongLong(&ok));
break;
}
case 2:
{
bool ok1 = false;
bool ok2 = false;
const QString& lowString = numbers.front();
const QString& highString = numbers.back();
const PDFInteger low = lowString.toLongLong(&ok1);
const PDFInteger high = highString.toLongLong(&ok2);
ok = ok1 && ok2 && low <= high;
if (ok)
{
const PDFInteger count = high - low + 1;
result.resize(result.size() + count, 0);
std::iota(std::prev(result.end(), count), result.end(), low);
}
break;
}
default:
{
ok = true;
break;
}
}
// If error is detected, do not continue in parsing
if (!ok)
{
break;
}
// We must remove duplicate pages
qSort(result);
result.erase(std::unique(result.begin(), result.end()), result.end());
}
if (!ok)
{
result.clear();
}
break;
}
default:
break;
}
return result;
}
PDFRasterizerPool::PDFRasterizerPool(const PDFDocument* document,
const PDFFontCache* fontCache,
const PDFCMS* cms,
const PDFOptionalContentActivity* optionalContentActivity,
PDFRenderer::Features features,
const PDFMeshQualitySettings& meshQualitySettings,
int rasterizerCount,
bool useOpenGL,
const QSurfaceFormat& surfaceFormat,
QObject* parent) :
BaseClass(parent),
m_document(document),
m_fontCache(fontCache),
m_cms(cms),
m_optionalContentActivity(optionalContentActivity),
m_features(features),
m_meshQualitySettings(meshQualitySettings),
m_semaphore(rasterizerCount)
{
m_rasterizers.reserve(rasterizerCount);
for (int i = 0; i < rasterizerCount; ++i)
{
m_rasterizers.push_back(new PDFRasterizer(this));
m_rasterizers.back()->reset(useOpenGL, surfaceFormat);
}
}
} // namespace pdf

View File

@ -1,4 +1,4 @@
// Copyright (C) 2019-2020 Jakub Melka
// Copyright (C) 2019-2020 Jakub Melka
//
// This file is part of PdfForQt.
//
@ -22,6 +22,8 @@
#include "pdfexception.h"
#include "pdfmeshqualitysettings.h"
#include <QMutex>
#include <QSemaphore>
#include <QImageWriter>
#include <QSurfaceFormat>
@ -33,6 +35,7 @@ class QOpenGLFramebufferObject;
namespace pdf
{
class PDFCMS;
class PDFProgress;
class PDFFontCache;
class PDFPrecompiledPage;
class PDFOptionalContentActivity;
@ -105,6 +108,7 @@ private:
/// Renders PDF pages to bitmap images (QImage). It can use OpenGL for painting,
/// if it is enabled, if this is the case, offscreen rendering to framebuffer
/// is used.
/// \note Construct this object only in main GUI thread
class PDFRasterizer : public QObject
{
Q_OBJECT
@ -114,7 +118,7 @@ private:
public:
explicit PDFRasterizer(QObject* parent);
~PDFRasterizer();
virtual ~PDFRasterizer() override;
/// Resets the renderer. This function must be called from main GUI thread,
/// it cannot be called from deferred threads, because it can create hidden
@ -155,6 +159,83 @@ private:
QOpenGLFramebufferObject* m_fbo;
};
/// Pool of page image renderers. It can use predefined number of renderers to
/// render page images asynchronously. You can use this object in two ways -
/// first one is as standard object pool, second one is to directly render
/// page images asynchronously.
class PDFFORQTLIBSHARED_EXPORT PDFRasterizerPool : public QObject
{
Q_OBJECT
private:
using BaseClass = QObject;
public:
using PageImageSizeGetter = std::function<QSize(const PDFPage*)>;
using ProcessImageMethod = std::function<void(PDFInteger, QImage&&)>;
/// Creates new rasterizer pool
/// \param document Document
/// \param fontCache Font cache
/// \param cms Color management system
/// \param optionalContentActivity Optional content activity
/// \param features Renderer features
/// \param meshQualitySettings Mesh quality settings
/// \param rasterizerCount Number of rasterizers
/// \param useOpenGL Use OpenGL for rendering?
/// \param surfaceFormat Surface format
/// \param parent Parent object
explicit PDFRasterizerPool(const PDFDocument* document,
const PDFFontCache* fontCache,
const PDFCMS* cms,
const PDFOptionalContentActivity* optionalContentActivity,
PDFRenderer::Features features,
const PDFMeshQualitySettings& meshQualitySettings,
int rasterizerCount,
bool useOpenGL,
const QSurfaceFormat& surfaceFormat,
QObject* parent);
/// Acquire rasterizer. This function is thread safe.
PDFRasterizer* acquire();
/// Return back (release) rasterizer into rasterizer pool
/// This function is thread safe.
/// \param rasterizer Rasterizer
void release(PDFRasterizer* rasterizer);
/// Renders pages asynchronously to images, using given page indices,
/// function which returns rendered size and process image function,
/// which processes rendered images.
/// \param pageIndices Page indices for rendered pages
/// \param imageSizeGetter Getter, which computes image size from page index
/// \param processImage Method, which processes rendered page images
/// \param progress Progress indicator
void render(const std::vector<PDFInteger>& pageIndices,
const PageImageSizeGetter& imageSizeGetter,
const ProcessImageMethod& processImage,
PDFProgress* progress);
/// Returns default rasterizer count
static int getDefaultRasterizerCount();
signals:
void renderError(PDFRenderError error);
private:
const PDFDocument* m_document;
const PDFFontCache* m_fontCache;
const PDFCMS* m_cms;
const PDFOptionalContentActivity* m_optionalContentActivity;
PDFRenderer::Features m_features;
const PDFMeshQualitySettings& m_meshQualitySettings;
QSemaphore m_semaphore;
QMutex m_mutex;
std::vector<PDFRasterizer*> m_rasterizers;
};
/// Settings object for image writer
class PDFFORQTLIBSHARED_EXPORT PDFImageWriterSettings
{
@ -211,7 +292,7 @@ private:
class PDFFORQTLIBSHARED_EXPORT PDFPageImageExportSettings
{
public:
explicit PDFPageImageExportSettings();
explicit PDFPageImageExportSettings(const PDFDocument* document);
enum class PageSelectionMode
{
@ -246,7 +327,20 @@ public:
int getPixelResolution() const;
void setPixelResolution(int pixelResolution);
/// Validates the settings, if they can be used for image generation
bool validate(QString* errorMessagePtr);
/// Returns list of selected pages
std::vector<PDFInteger> getPages() const;
static constexpr int getMinDPIResolution() { return 72; }
static constexpr int getMaxDPIResolution() { return 6000; }
static constexpr int getMinPixelResolution() { return 100; }
static constexpr int getMaxPixelResolution() { return 16384; }
private:
const PDFDocument* m_document;
ResolutionMode m_resolutionMode = ResolutionMode::DPI;
PageSelectionMode m_pageSelectionMode = PageSelectionMode::All;
QString m_directory;

View File

@ -25,6 +25,7 @@ int main(int argc, char *argv[])
{
QApplication::setAttribute(Qt::AA_CompressHighFrequencyEvents, true);
QApplication::setAttribute(Qt::AA_DisableHighDpiScaling, true);
QApplication::setAttribute(Qt::AA_DontCheckOpenGLContextThreadAffinity, true);
QApplication application(argc, argv);
QCoreApplication::setOrganizationName("MelkaJ");

View File

@ -18,17 +18,29 @@
#include "pdfrendertoimagesdialog.h"
#include "ui_pdfrendertoimagesdialog.h"
#include "pdfcms.h"
#include "pdfutils.h"
#include "pdfwidgetutils.h"
#include "pdfoptionalcontent.h"
#include "pdfdrawspacecontroller.h"
#include <QFileDialog>
#include <QMessageBox>
#include <QPushButton>
namespace pdfviewer
{
PDFRenderToImagesDialog::PDFRenderToImagesDialog(QWidget* parent) :
PDFRenderToImagesDialog::PDFRenderToImagesDialog(const pdf::PDFDocument* document,
pdf::PDFDrawWidgetProxy* proxy,
pdf::PDFProgress* progress,
QWidget* parent) :
QDialog(parent),
ui(new Ui::PDFRenderToImagesDialog),
m_document(document),
m_proxy(proxy),
m_progress(progress),
m_imageExportSettings(document),
m_isLoadingData(false)
{
ui->setupUi(this);
@ -56,6 +68,9 @@ PDFRenderToImagesDialog::PDFRenderToImagesDialog(QWidget* parent) :
connect(ui->optimizedWriteCheckBox, &QCheckBox::clicked, this, &PDFRenderToImagesDialog::onOptimizedWriteChanged);
connect(ui->progressiveScanWriteCheckBox, &QCheckBox::clicked, this, &PDFRenderToImagesDialog::onProgressiveScanWriteChanged);
ui->resolutionDPIEdit->setRange(pdf::PDFPageImageExportSettings::getMinDPIResolution(), pdf::PDFPageImageExportSettings::getMaxDPIResolution());
ui->resolutionPixelsEdit->setRange(pdf::PDFPageImageExportSettings::getMinPixelResolution(), pdf::PDFPageImageExportSettings::getMaxPixelResolution());
loadImageWriterSettings();
loadImageExportSettings();
@ -219,6 +234,11 @@ void PDFRenderToImagesDialog::onProgressiveScanWriteChanged(bool value)
m_imageWriterSettings.setProgressiveScanWrite(value);
}
void PDFRenderToImagesDialog::onRenderError(pdf::PDFRenderError error)
{
ui->progressMessagesEdit->setPlainText(QString("%1\n%2").arg(ui->progressMessagesEdit->toPlainText()).arg(error.message));
}
void PDFRenderToImagesDialog::on_selectDirectoryButton_clicked()
{
QString directory = QFileDialog::getExistingDirectory(this, tr("Select output directory"), ui->directoryEdit->text());
@ -228,6 +248,67 @@ void PDFRenderToImagesDialog::on_selectDirectoryButton_clicked()
}
}
void PDFRenderToImagesDialog::on_buttonBox_clicked(QAbstractButton* button)
{
if (button == ui->buttonBox->button(QDialogButtonBox::Apply))
{
QString message;
if (m_imageExportSettings.validate(&message))
{
// We are ready to render the document
std::vector<pdf::PDFInteger> pageIndices = m_imageExportSettings.getPages();
pdf::PDFOptionalContentActivity optionalContentActivity(m_document, pdf::OCUsage::Export, nullptr);
pdf::PDFCMSPointer cms = m_proxy->getCMSManager()->getCurrentCMS();
pdf::PDFRasterizerPool rasterizerPool(m_document, m_proxy->getFontCache(), cms.data(),
&optionalContentActivity, m_proxy->getFeatures(), m_proxy->getMeshQualitySettings(),
pdf::PDFRasterizerPool::getDefaultRasterizerCount(), m_proxy->isUsingOpenGL(), m_proxy->getSurfaceFormat(), this);
connect(&rasterizerPool, &pdf::PDFRasterizerPool::renderError, this, &PDFRenderToImagesDialog::onRenderError);
auto imageSizeGetter = [this](const pdf::PDFPage* page) -> QSize
{
Q_ASSERT(page);
switch (m_imageExportSettings.getResolutionMode())
{
case pdf::PDFPageImageExportSettings::ResolutionMode::DPI:
{
QSizeF size = page->getRotatedMediaBox().size() * m_imageExportSettings.getDpiResolution();
return size.toSize();
}
case pdf::PDFPageImageExportSettings::ResolutionMode::Pixels:
{
int pixelResolution = m_imageExportSettings.getPixelResolution();
QSizeF size = page->getRotatedMediaBox().size().scaled(pixelResolution, pixelResolution, Qt::KeepAspectRatio);
return size.toSize();
}
default:
{
Q_ASSERT(false);
break;
}
}
return QSize();
};
auto processImage = [](const pdf::PDFInteger pageIndex, QImage&& image)
{
Q_UNUSED(pageIndex);
Q_UNUSED(image);
};
setEnabled(false);
rasterizerPool.render(pageIndices, imageSizeGetter, processImage, m_progress);
setEnabled(true);
}
else
{
QMessageBox::critical(this, tr("Error"), message);
}
}
}
} // namespace pdfviewer

View File

@ -22,11 +22,19 @@
#include <QDialog>
class QAbstractButton;
namespace Ui
{
class PDFRenderToImagesDialog;
}
namespace pdf
{
class PDFProgress;
class PDFDrawWidgetProxy;
}
namespace pdfviewer
{
@ -35,11 +43,15 @@ class PDFRenderToImagesDialog : public QDialog
Q_OBJECT
public:
explicit PDFRenderToImagesDialog(QWidget* parent);
explicit PDFRenderToImagesDialog(const pdf::PDFDocument* document,
pdf::PDFDrawWidgetProxy* proxy,
pdf::PDFProgress* progress,
QWidget* parent);
virtual ~PDFRenderToImagesDialog() override;
private slots:
void on_selectDirectoryButton_clicked();
void on_buttonBox_clicked(QAbstractButton* button);
private:
/// Loads image writer settings to the ui
@ -62,8 +74,12 @@ private:
void onGammaChanged(double value);
void onOptimizedWriteChanged(bool value);
void onProgressiveScanWriteChanged(bool value);
void onRenderError(pdf::PDFRenderError error);
Ui::PDFRenderToImagesDialog* ui;
const pdf::PDFDocument* m_document;
pdf::PDFDrawWidgetProxy* m_proxy;
pdf::PDFProgress* m_progress;
pdf::PDFImageWriterSettings m_imageWriterSettings;
pdf::PDFPageImageExportSettings m_imageExportSettings;
bool m_isLoadingData;

View File

@ -7,7 +7,7 @@
<x>0</x>
<y>0</y>
<width>690</width>
<height>593</height>
<height>602</height>
</rect>
</property>
<property name="windowTitle">
@ -234,7 +234,14 @@
</widget>
</item>
<item>
<widget class="QPlainTextEdit" name="plainTextEdit"/>
<widget class="QPlainTextEdit" name="progressMessagesEdit">
<property name="undoRedoEnabled">
<bool>false</bool>
</property>
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">

View File

@ -1279,11 +1279,8 @@ void PDFViewerMainWindow::on_actionPrint_triggered()
void PDFViewerMainWindow::on_actionRender_to_Images_triggered()
{
PDFRenderToImagesDialog dialog(this);
if (dialog.exec() == QDialog::Accepted)
{
}
PDFRenderToImagesDialog dialog(m_pdfDocument.data(), m_pdfWidget->getDrawWidgetProxy(), m_progress, this);
dialog.exec();
}
} // namespace pdfviewer