Введение
Предыдущие статьи цикла фокусировались на генерации новых PDF-документов «с нуля». Но часто нужна обратная задача — открыть и отобразить уже существующий PDF-файл внутри Qt-приложения: предпросмотр сканированного документа, встроенный просмотрщик руководства пользователя, выборочный рендеринг страниц для миниатюр. Модуль Qt PDF предоставляет QPdfDocument именно для этой задачи — загрузки и рендеринга существующих PDF-файлов без необходимости в стороннем движке вроде Poppler. В статье разберём базовое использование.
Концепция
QPdfDocument::load() открывает PDF-файл и предоставляет доступ к метаданным (число страниц, заголовок, автор) и, что важнее всего, к методу render(pageIndex, imageSize), который растеризует конкретную страницу документа в QImage заданного разрешения — это даёт возможность отображать страницы PDF как обычные изображения в QLabel/QGraphicsView/QML Image, либо генерировать миниатюры для навигации по многостраничному документу.
Пример кода: Работа с существующими PDF
#include <QPdfDocument>
#include <QImage>
#include <QLabel>
#include <QDebug>
class PdfViewer : public QWidget
{
Q_OBJECT
public:
explicit PdfViewer(QWidget *parent = nullptr) : QWidget(parent)
{
m_pageLabel = new QLabel(this);
auto *layout = new QVBoxLayout(this);
layout->addWidget(m_pageLabel);
connect(&m_document, &QPdfDocument::statusChanged, this, &PdfViewer::onDocumentStatusChanged);
}
void openFile(const QString &filePath)
{
const auto error = m_document.load(filePath);
if (error != QPdfDocument::Error::None) {
qWarning() << "Не удалось открыть PDF:" << static_cast<int>(error);
return;
}
}
void showPage(int pageIndex)
{
if (pageIndex < 0 || pageIndex >= m_document.pageCount()) {
return;
}
const QSizeF pointSize = m_document.pagePointSize(pageIndex);
// Рендерим с разрешением, соответствующим примерно 150 DPI для предпросмотра на экране
const double scaleFactor = 150.0 / 72.0; // 72 точки на дюйм — базовая единица PDF
const QSize renderSize(pointSize.width() * scaleFactor, pointSize.height() * scaleFactor);
const QImage pageImage = m_document.render(pageIndex, renderSize);
m_pageLabel->setPixmap(QPixmap::fromImage(pageImage));
m_currentPage = pageIndex;
}
private slots:
void onDocumentStatusChanged(QPdfDocument::Status status)
{
if (status == QPdfDocument::Status::Ready) {
qDebug() << "PDF загружен, страниц:" << m_document.pageCount()
<< "заголовок:" << m_document.metaData(QPdfDocument::MetaDataField::Title).toString();
showPage(0);
} else if (status == QPdfDocument::Status::Error) {
qWarning() << "Ошибка при загрузке PDF-документа";
}
}
private:
QPdfDocument m_document;
QLabel *m_pageLabel;
int m_currentPage = 0;
};
// Генерация миниатюр всех страниц для панели навигации
QList<QPixmap> generateThumbnails(QPdfDocument &document, int thumbnailWidth = 100)
{
QList<QPixmap> thumbnails;
for (int i = 0; i < document.pageCount(); ++i) {
const QSizeF pointSize = document.pagePointSize(i);
const double aspectRatio = pointSize.height() / pointSize.width();
const QSize thumbSize(thumbnailWidth, static_cast<int>(thumbnailWidth * aspectRatio));
const QImage thumbImage = document.render(i, thumbSize);
thumbnails.append(QPixmap::fromImage(thumbImage));
}
return thumbnails;
}
Пояснения к коду
Загрузка PDF через QPdfDocument::load() асинхронна по своей природе — статус документа меняется через сигнал statusChanged, и реальная работа с содержимым (получение числа страниц, рендеринг) должна начинаться только после того, как статус перешёл в QPdfDocument::Status::Ready, что отражено в обработчике onDocumentStatusChanged, вызывающем showPage(0) только при успешной готовности документа.
pagePointSize() возвращает размер страницы в «точках» PDF (где 72 точки традиционно соответствуют одному дюйму, унаследовано из истории PostScript) — для получения изображения с конкретным желаемым разрешением экрана (150 DPI в примере) нужно явно пересчитать размер через коэффициент scaleFactor. generateThumbnails() показывает практическое применение пакетного рендеринга — миниатюры всех страниц документа, обычно используемые для боковой панели навигации в просмотрщиках PDF, с сохранением правильного соотношения сторон каждой страницы через aspectRatio.
Подводные камни
- Рендеринг страниц с высоким разрешением для очень многостраничных документов потребляет значительный объём памяти, если все рендеры сохраняются одновременно (как в
generateThumbnails()для документа из тысяч страниц) — для документов с большим количеством страниц миниатюры стоит генерировать «лениво», по мере того как они реально становятся видимыми в панели навигации (аналогично виртуализации списков, разобранной в основном цикле статей про QML), а не все сразу при открытии файла. - PDF-документы, защищённые паролем, требуют дополнительной обработки через
setPassword()или соответствующий параметр при загрузке — попытка открыть защищённый файл без пароля завершится ошибкой статуса, отличной от обычногоNone, и приложение должно явно обрабатывать этот случай, предлагая пользователю ввести пароль, а не просто сообщая об общей ошибке загрузки. QPdfDocumentне предоставляет прямого доступа к тексту PDF для полнотекстового поиска «из коробки» в самых ранних версиях модуля Qt PDF — для функциональности поиска текста внутри PDF (а не только визуального отображения страниц) нужно проверять наличие соответствующего API (QPdfSearchModelв более новых версиях Qt) для конкретной используемой версии Qt, поскольку возможности модуля существенно эволюционировали между релизами.- Векторное содержимое PDF (текст, линии) растеризуется в
QImageпри рендеринге страницы, теряя присущую векторной графике масштабируемость без потери качества — при значительном увеличении масштаба отображения растеризованной страницы (например, пользователь приближает мелкий текст) видна пикселизация, если изображение не было заранее отрендерено в достаточно высоком разрешении; для интерактивного масштабирования без видимой потери качества нужно перерендеривать страницу в новом, увеличенном разрешении при каждом значительном изменении масштаба, а не растягивать уже готовое растровое изображение.