Пользовательский интерфейс в Qt

Работа с существующими PDF: QPdfDocument и рендеринг страниц

Введение

Предыдущие статьи цикла фокусировались на генерации новых 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 при рендеринге страницы, теряя присущую векторной графике масштабируемость без потери качества — при значительном увеличении масштаба отображения растеризованной страницы (например, пользователь приближает мелкий текст) видна пикселизация, если изображение не было заранее отрендерено в достаточно высоком разрешении; для интерактивного масштабирования без видимой потери качества нужно перерендеривать страницу в новом, увеличенном разрешении при каждом значительном изменении масштаба, а не растягивать уже готовое растровое изображение.