diff --git a/PdfForQtLib/PdfForQtLib.pro b/PdfForQtLib/PdfForQtLib.pro index bd46135..bf7658a 100644 --- a/PdfForQtLib/PdfForQtLib.pro +++ b/PdfForQtLib/PdfForQtLib.pro @@ -225,7 +225,8 @@ qt_libraries.files = $$[QT_INSTALL_BINS]/Qt?Widgets$${SUFFIX}.dll \ $$[QT_INSTALL_BINS]/Qt?Core$${SUFFIX}.dll \ $$[QT_INSTALL_BINS]/Qt?WinExtras$${SUFFIX}.dll \ $$[QT_INSTALL_BINS]/Qt?Svg$${SUFFIX}.dll \ - $$[QT_INSTALL_BINS]/Qt?PrintSupport$${SUFFIX}.dll + $$[QT_INSTALL_BINS]/Qt?PrintSupport$${SUFFIX}.dll \ + $$[QT_INSTALL_BINS]/Qt?TextToSpeech$${SUFFIX}.dll qt_libraries.path = $$DESTDIR/install INSTALLS += qt_libraries @@ -240,3 +241,8 @@ INSTALLS += qt_plugin_iconengine qt_plugin_printsupport.files = $$[QT_INSTALL_PLUGINS]/printsupport/windowsprintersupport$${SUFFIX}.dll qt_plugin_printsupport.path = $$DESTDIR/install/printsupport INSTALLS += qt_plugin_printsupport + +qt_plugin_texttospeech.files = $$[QT_INSTALL_PLUGINS]/texttospeech/qtexttospeech_sapi$${SUFFIX}.dll +qt_plugin_texttospeech.path = $$DESTDIR/install/texttospeech +INSTALLS += qt_plugin_texttospeech + diff --git a/PdfForQtLib/sources/pdftextlayout.h b/PdfForQtLib/sources/pdftextlayout.h index cd06f68..52b16fb 100644 --- a/PdfForQtLib/sources/pdftextlayout.h +++ b/PdfForQtLib/sources/pdftextlayout.h @@ -272,7 +272,7 @@ using PDFTextFlows = std::vector; /// This class represents a portion of continuous text on the page. It can /// consists of multiple blocks (which follow reading order). -class PDFTextFlow +class PDFFORQTLIBSHARED_EXPORT PDFTextFlow { public: @@ -294,6 +294,9 @@ public: /// \param expression Regular expression to be matched PDFFindResults find(const QRegularExpression& expression) const; + /// Returns whole text for this text flow + QString getText() const { return m_text; } + /// Returns text form character pointers /// \param begin Begin character /// \param end End character @@ -327,7 +330,7 @@ private: /// Text layout of single page. Can handle various fonts, various angles of lines /// and vertically oriented text. It performs the "docstrum" algorithm. -class PDFTextLayout +class PDFFORQTLIBSHARED_EXPORT PDFTextLayout { public: explicit PDFTextLayout(); @@ -498,4 +501,6 @@ private: } // namespace pdf +Q_DECLARE_OPERATORS_FOR_FLAGS(pdf::PDFTextFlow::FlowFlags) + #endif // PDFTEXTLAYOUT_H diff --git a/PdfForQtLib/sources/pdfutils.h b/PdfForQtLib/sources/pdfutils.h index 35a6b98..c01f2f7 100644 --- a/PdfForQtLib/sources/pdfutils.h +++ b/PdfForQtLib/sources/pdfutils.h @@ -445,6 +445,32 @@ inline QColor invertColor(QColor color) return QColor::fromRgbF(r, g, b, a); } +/// Performs linear interpolation of interval [x1, x2] to interval [y1, y2], +/// using formula y = y1 + (x - x1) * (y2 - y1) / (x2 - x1), transformed +/// to formula y = k * x + q, where q = y1 - x1 * k and +/// k = (y2 - y1) / (x2 - x1). +template +class PDFLinearInterpolation +{ +public: + constexpr inline PDFLinearInterpolation(T x1, T x2, T y1, T y2) : + m_k((y2 - y1) / (x2 - x1)), + m_q(y1 - x1 * m_k) + { + + } + + /// Maps value from x interval to y interval + constexpr inline T operator()(T x) const + { + return m_k * x + m_q; + } + +private: + T m_k; + T m_q; +}; + } // namespace pdf #endif // PDFUTILS_H diff --git a/PdfForQtViewer/PdfForQtViewer.pro b/PdfForQtViewer/PdfForQtViewer.pro index 00750f3..fecc47f 100644 --- a/PdfForQtViewer/PdfForQtViewer.pro +++ b/PdfForQtViewer/PdfForQtViewer.pro @@ -41,6 +41,7 @@ SOURCES += \ pdfrendertoimagesdialog.cpp \ pdfsendmail.cpp \ pdfsidebarwidget.cpp \ + pdftexttospeech.cpp \ pdfviewermainwindow.cpp \ pdfviewersettings.cpp \ pdfviewersettingsdialog.cpp \ @@ -54,6 +55,7 @@ HEADERS += \ pdfrendertoimagesdialog.h \ pdfsendmail.h \ pdfsidebarwidget.h \ + pdftexttospeech.h \ pdfviewermainwindow.h \ pdfviewersettings.h \ pdfviewersettingsdialog.h \ diff --git a/PdfForQtViewer/pdfforqtviewer.qrc b/PdfForQtViewer/pdfforqtviewer.qrc index 8f5b6d2..a906c05 100644 --- a/PdfForQtViewer/pdfforqtviewer.qrc +++ b/PdfForQtViewer/pdfforqtviewer.qrc @@ -37,5 +37,8 @@ resources/rotate-right.svg resources/print.svg resources/speech.svg + resources/pause.svg + resources/play.svg + resources/stop.svg diff --git a/PdfForQtViewer/pdfsidebarwidget.cpp b/PdfForQtViewer/pdfsidebarwidget.cpp index faddffb..764b13b 100644 --- a/PdfForQtViewer/pdfsidebarwidget.cpp +++ b/PdfForQtViewer/pdfsidebarwidget.cpp @@ -19,6 +19,7 @@ #include "ui_pdfsidebarwidget.h" #include "pdfwidgetutils.h" +#include "pdftexttospeech.h" #include "pdfdocument.h" #include "pdfitemmodels.h" @@ -40,12 +41,14 @@ constexpr const char* STYLESHEET = "QPushButton:disabled { background-color: #404040; color: #000000; }" "QPushButton:checked { background-color: #808080; color: #FFFFFF; }" "QWidget#thumbnailsToolbarWidget { background-color: #F0F0F0 }" + "QWidget#speechPage { background-color: #F0F0F0 }" "QWidget#PDFSidebarWidget { background-color: #404040; background: green;}"; -PDFSidebarWidget::PDFSidebarWidget(pdf::PDFDrawWidgetProxy* proxy, QWidget* parent) : +PDFSidebarWidget::PDFSidebarWidget(pdf::PDFDrawWidgetProxy* proxy, PDFTextToSpeech* textToSpeech, QWidget* parent) : QWidget(parent), ui(new Ui::PDFSidebarWidget), m_proxy(proxy), + m_textToSpeech(textToSpeech), m_outlineTreeModel(nullptr), m_thumbnailsModel(nullptr), m_optionalContentTreeModel(nullptr), @@ -93,6 +96,7 @@ PDFSidebarWidget::PDFSidebarWidget(pdf::PDFDrawWidgetProxy* proxy, QWidget* pare m_pageInfo[Bookmarks] = { ui->bookmarksButton, ui->bookmarksPage }; m_pageInfo[Thumbnails] = { ui->thumbnailsButton, ui->thumbnailsPage }; m_pageInfo[Attachments] = { ui->attachmentsButton, ui->attachmentsPage }; + m_pageInfo[Speech] = { ui->speechButton, ui->speechPage }; for (const auto& pageInfo : m_pageInfo) { @@ -102,6 +106,10 @@ PDFSidebarWidget::PDFSidebarWidget(pdf::PDFDrawWidgetProxy* proxy, QWidget* pare } } + m_textToSpeech->initializeUI(ui->speechLocaleComboBox, ui->speechVoiceComboBox, + ui->speechRateEdit, ui->speechPitchEdit, ui->speechVolumeEdit, + ui->speechPlayButton, ui->speechPauseButton, ui->speechStopButton, ui->speechSynchronizeButton); + selectPage(Invalid); updateButtons(); } @@ -220,6 +228,9 @@ bool PDFSidebarWidget::isEmpty(Page page) const case Attachments: return m_attachmentsTreeModel->isEmpty(); + case Speech: + return !m_textToSpeech->isValid(); + default: Q_ASSERT(false); break; diff --git a/PdfForQtViewer/pdfsidebarwidget.h b/PdfForQtViewer/pdfsidebarwidget.h index b342b94..13d8b59 100644 --- a/PdfForQtViewer/pdfsidebarwidget.h +++ b/PdfForQtViewer/pdfsidebarwidget.h @@ -45,13 +45,14 @@ class PDFOptionalContentTreeItemModel; namespace pdfviewer { +class PDFTextToSpeech; class PDFSidebarWidget : public QWidget { Q_OBJECT public: - explicit PDFSidebarWidget(pdf::PDFDrawWidgetProxy* proxy, QWidget* parent = nullptr); + explicit PDFSidebarWidget(pdf::PDFDrawWidgetProxy* proxy, PDFTextToSpeech* textToSpeech, QWidget* parent); virtual ~PDFSidebarWidget() override; virtual void paintEvent(QPaintEvent* event) override; @@ -64,6 +65,7 @@ public: Thumbnails, OptionalContent, Attachments, + Speech, _END }; @@ -105,6 +107,7 @@ private: Ui::PDFSidebarWidget* ui; pdf::PDFDrawWidgetProxy* m_proxy; + PDFTextToSpeech* m_textToSpeech; pdf::PDFOutlineTreeItemModel* m_outlineTreeModel; pdf::PDFThumbnailsItemModel* m_thumbnailsModel; pdf::PDFOptionalContentTreeItemModel* m_optionalContentTreeModel; diff --git a/PdfForQtViewer/pdfsidebarwidget.ui b/PdfForQtViewer/pdfsidebarwidget.ui index 62ad5f0..9f7fdf3 100644 --- a/PdfForQtViewer/pdfsidebarwidget.ui +++ b/PdfForQtViewer/pdfsidebarwidget.ui @@ -80,6 +80,19 @@ + + + + Speech + + + true + + + true + + + @@ -98,7 +111,7 @@ - 2 + 5 @@ -238,6 +251,142 @@ + + + + + + + + + + + + + + -10 + + + 10 + + + Qt::Horizontal + + + + + + + 100 + + + 100 + + + Qt::Horizontal + + + + + + + -10 + + + 10 + + + Qt::Horizontal + + + + + + + Rate + + + + + + + Pitch + + + + + + + Volume + + + + + + + + + + + + :/resources/play.svg:/resources/play.svg + + + + + + + + :/resources/pause.svg:/resources/pause.svg + + + + + + + + :/resources/stop.svg:/resources/stop.svg + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + :/resources/synchronize.svg:/resources/synchronize.svg + + + + + + + + + Qt::Vertical + + + + 20 + 393 + + + + + + diff --git a/PdfForQtViewer/pdftexttospeech.cpp b/PdfForQtViewer/pdftexttospeech.cpp new file mode 100644 index 0000000..8932823 --- /dev/null +++ b/PdfForQtViewer/pdftexttospeech.cpp @@ -0,0 +1,498 @@ +// Copyright (C) 2020 Jakub Melka +// +// This file is part of PdfForQt. +// +// PdfForQt 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 +// (at your option) any later version. +// +// PdfForQt 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 PDFForQt. If not, see . + +#include "pdftexttospeech.h" +#include "pdfviewersettings.h" +#include "pdfdrawspacecontroller.h" +#include "pdfcompiler.h" +#include "pdfdrawwidget.h" + +#include +#include +#include +#include +#include + +namespace pdfviewer +{ + +PDFTextToSpeech::PDFTextToSpeech(QObject* parent) : + BaseClass(parent), + m_textToSpeech(nullptr), + m_document(nullptr), + m_proxy(nullptr), + m_state(Invalid), + m_initialized(false), + m_speechLocaleComboBox(nullptr), + m_speechVoiceComboBox(nullptr), + m_speechRateEdit(nullptr), + m_speechVolumeEdit(nullptr), + m_speechPitchEdit(nullptr), + m_speechPlayButton(nullptr), + m_speechPauseButton(nullptr), + m_speechStopButton(nullptr), + m_speechSynchronizeButton(nullptr) +{ + +} + +bool PDFTextToSpeech::isValid() const +{ + return m_document != nullptr; +} + +void PDFTextToSpeech::setDocument(const pdf::PDFDocument* document) +{ + if (m_document != document) + { + stop(); + m_document = document; + + if (m_textToSpeech) + { + m_state = m_document ? Ready : NoDocument; + } + else + { + // Set state to invalid, speech engine is not set + m_state = Invalid; + } + + updateUI(); + } +} + +void PDFTextToSpeech::setSettings(const PDFViewerSettings* viewerSettings) +{ + Q_ASSERT(viewerSettings); + + if (!m_initialized) + { + // This object is not initialized yet + return; + } + + // First, stop the engine + stop(); + + delete m_textToSpeech; + m_textToSpeech = nullptr; + + const PDFViewerSettings::Settings& settings = viewerSettings->getSettings(); + if (!settings.m_speechEngine.isEmpty()) + { + m_textToSpeech = new QTextToSpeech(settings.m_speechEngine); + connect(m_textToSpeech, &QTextToSpeech::stateChanged, this, &PDFTextToSpeech::updatePlay); + m_state = m_document ? Ready : NoDocument; + + QVector locales = m_textToSpeech->availableLocales(); + m_speechLocaleComboBox->setUpdatesEnabled(false); + m_speechLocaleComboBox->clear(); + for (const QLocale& locale : locales) + { + m_speechLocaleComboBox->addItem(QString("%1 (%2)").arg(locale.nativeLanguageName(), locale.nativeCountryName()), locale.name()); + } + m_speechLocaleComboBox->setUpdatesEnabled(true); + + QVector voices = m_textToSpeech->availableVoices(); + m_speechVoiceComboBox->setUpdatesEnabled(false); + m_speechVoiceComboBox->clear(); + for (const QVoice& voice : voices) + { + m_speechVoiceComboBox->addItem(QString("%1 (%2, %3)").arg(voice.name(), QVoice::genderName(voice.gender()), QVoice::ageName(voice.age())), voice.name()); + } + m_speechVoiceComboBox->setUpdatesEnabled(true); + } + else + { + // Set state to invalid, speech engine is not set + m_state = Invalid; + + m_speechLocaleComboBox->clear(); + m_speechVoiceComboBox->clear(); + } + + if (m_textToSpeech) + { + setLocale(settings.m_speechLocale); + setVoice(settings.m_speechVoice); + setRate(settings.m_speechRate); + setPitch(settings.m_speechPitch); + setVolume(settings.m_speechVolume); + } + + updateUI(); +} + +void PDFTextToSpeech::setProxy(const pdf::PDFDrawWidgetProxy* proxy) +{ + m_proxy = proxy; + pdf::PDFAsynchronousTextLayoutCompiler* compiler = m_proxy->getTextLayoutCompiler(); + connect(compiler, &pdf::PDFAsynchronousTextLayoutCompiler::textLayoutChanged, this, &PDFTextToSpeech::updatePlay); +} + +void PDFTextToSpeech::initializeUI(QComboBox* speechLocaleComboBox, + QComboBox* speechVoiceComboBox, + QSlider* speechRateEdit, + QSlider* speechVolumeEdit, + QSlider* speechPitchEdit, + QToolButton* speechPlayButton, + QToolButton* speechPauseButton, + QToolButton* speechStopButton, + QToolButton* speechSynchronizeButton) +{ + Q_ASSERT(speechLocaleComboBox); + Q_ASSERT(speechVoiceComboBox); + Q_ASSERT(speechRateEdit); + Q_ASSERT(speechVolumeEdit); + Q_ASSERT(speechPitchEdit); + Q_ASSERT(speechPlayButton); + Q_ASSERT(speechPauseButton); + Q_ASSERT(speechStopButton); + Q_ASSERT(speechSynchronizeButton); + + m_speechLocaleComboBox = speechLocaleComboBox; + m_speechVoiceComboBox = speechVoiceComboBox; + m_speechRateEdit = speechRateEdit; + m_speechVolumeEdit = speechVolumeEdit; + m_speechPitchEdit = speechPitchEdit; + m_speechPlayButton = speechPlayButton; + m_speechPauseButton = speechPauseButton; + m_speechStopButton = speechStopButton; + m_speechSynchronizeButton = speechSynchronizeButton; + + connect(m_speechLocaleComboBox, QOverload::of(&QComboBox::currentIndexChanged), this, &PDFTextToSpeech::onLocaleChanged); + connect(m_speechVoiceComboBox, QOverload::of(&QComboBox::currentIndexChanged), this, &PDFTextToSpeech::onVoiceChanged); + connect(m_speechRateEdit, &QSlider::valueChanged, this, &PDFTextToSpeech::onRateChanged); + connect(m_speechPitchEdit, &QSlider::valueChanged, this, &PDFTextToSpeech::onPitchChanged); + connect(m_speechVolumeEdit, &QSlider::valueChanged, this, &PDFTextToSpeech::onVolumeChanged); + connect(m_speechPlayButton, &QToolButton::clicked, this, &PDFTextToSpeech::onPlayClicked); + connect(m_speechPauseButton, &QToolButton::clicked, this, &PDFTextToSpeech::onPauseClicked); + connect(m_speechStopButton, &QToolButton::clicked, this, &PDFTextToSpeech::onStopClicked); + + m_initialized = true; +} + +void PDFTextToSpeech::updateUI() +{ + bool enableControls = false; + bool enablePlay = false; + bool enablePause = false; + bool enableStop = false; + + switch (m_state) + { + case pdfviewer::PDFTextToSpeech::Invalid: + { + enableControls = false; + enablePlay = false; + enablePause = false; + enableStop = false; + break; + } + + case pdfviewer::PDFTextToSpeech::NoDocument: + { + enableControls = true; + enablePlay = false; + enablePause = false; + enableStop = false; + break; + } + + case pdfviewer::PDFTextToSpeech::Ready: + { + enableControls = true; + enablePlay = true; + enablePause = false; + enableStop = false; + break; + } + + case pdfviewer::PDFTextToSpeech::Playing: + { + enableControls = true; + enablePlay = false; + enablePause = true; + enableStop = true; + break; + } + + case pdfviewer::PDFTextToSpeech::Paused: + { + enableControls = true; + enablePlay = true; + enablePause = false; + enableStop = true; + break; + } + + case pdfviewer::PDFTextToSpeech::Error: + { + enableControls = false; + enablePlay = false; + enablePause = false; + enableStop = false; + break; + } + + default: + Q_ASSERT(false); + break; + } + + m_speechLocaleComboBox->setEnabled(enableControls && m_speechLocaleComboBox->count() > 0); + m_speechVoiceComboBox->setEnabled(enableControls && m_speechVoiceComboBox->count() > 0); + m_speechRateEdit->setEnabled(enableControls); + m_speechVolumeEdit->setEnabled(enableControls); + m_speechPitchEdit->setEnabled(enableControls); + m_speechPlayButton->setEnabled(enablePlay); + m_speechPauseButton->setEnabled(enablePause); + m_speechStopButton->setEnabled(enableStop); + m_speechSynchronizeButton->setEnabled(enableControls); +} + +void PDFTextToSpeech::stop() +{ + switch (m_state) + { + case Playing: + case Paused: + { + m_textToSpeech->stop(); + m_currentTextFlowIndex = 0; + m_currentPage = 0; + m_currentTextLayout = pdf::PDFTextLayout(); + m_textFlows = pdf::PDFTextFlows(); + m_state = Ready; + break; + } + + default: + break; + } + + updateUI(); +} + +void PDFTextToSpeech::setLocale(const QString& locale) +{ + m_speechLocaleComboBox->setCurrentIndex(m_speechLocaleComboBox->findData(locale)); +} + +void PDFTextToSpeech::setVoice(const QString& voice) +{ + m_speechVoiceComboBox->setCurrentIndex(m_speechVoiceComboBox->findData(voice)); +} + +void PDFTextToSpeech::setRate(const double rate) +{ + pdf::PDFLinearInterpolation interpolation(-1.0, 1.0, m_speechRateEdit->minimum(), m_speechRateEdit->maximum()); + m_speechRateEdit->setValue(qRound(interpolation(rate))); +} + +void PDFTextToSpeech::setPitch(const double pitch) +{ + pdf::PDFLinearInterpolation interpolation(-1.0, 1.0, m_speechPitchEdit->minimum(), m_speechPitchEdit->maximum()); + m_speechPitchEdit->setValue(qRound(interpolation(pitch))); +} + +void PDFTextToSpeech::setVolume(const double volume) +{ + pdf::PDFLinearInterpolation interpolation(0.0, 1.0, m_speechVolumeEdit->minimum(), m_speechVolumeEdit->maximum()); + m_speechVolumeEdit->setValue(qRound(interpolation(volume))); +} + +void PDFTextToSpeech::onLocaleChanged() +{ + if (m_textToSpeech) + { + m_textToSpeech->setLocale(QLocale(m_speechLocaleComboBox->currentData().toString())); + } +} + +void PDFTextToSpeech::onVoiceChanged() +{ + if (m_textToSpeech) + { + QString voice = m_speechVoiceComboBox->currentData().toString(); + for (const QVoice& voiceObject : m_textToSpeech->availableVoices()) + { + if (voiceObject.name() == voice) + { + m_textToSpeech->setVoice(voiceObject); + } + } + } +} + +void PDFTextToSpeech::onRateChanged(int rate) +{ + if (m_textToSpeech) + { + pdf::PDFLinearInterpolation interpolation(m_speechRateEdit->minimum(), m_speechRateEdit->maximum(), -1.0, 1.0); + m_textToSpeech->setRate(interpolation(rate)); + } +} + +void PDFTextToSpeech::onPitchChanged(int pitch) +{ + if (m_textToSpeech) + { + pdf::PDFLinearInterpolation interpolation(m_speechPitchEdit->minimum(), m_speechPitchEdit->maximum(), -1.0, 1.0); + m_textToSpeech->setPitch(interpolation(pitch)); + } +} + +void PDFTextToSpeech::onVolumeChanged(int volume) +{ + if (m_textToSpeech) + { + pdf::PDFLinearInterpolation interpolation(m_speechVolumeEdit->minimum(), m_speechVolumeEdit->maximum(), 0.0, 1.0); + m_textToSpeech->setVolume(interpolation(volume)); + } +} + +void PDFTextToSpeech::onPlayClicked() +{ + switch (m_state) + { + case Paused: + { + m_textToSpeech->resume(); + m_state = Playing; + updatePlay(); + break; + } + + case Ready: + { + m_state = Playing; + m_currentTextFlowIndex = std::numeric_limits::max(); + m_currentPage = -1; + updatePlay(); + break; + } + } + + updateUI(); +} + +void PDFTextToSpeech::onPauseClicked() +{ + Q_ASSERT(m_state == Playing); + + if (m_state == Playing) + { + m_textToSpeech->pause(); + m_state = Paused; + updateUI(); + } +} + +void PDFTextToSpeech::onStopClicked() +{ + stop(); +} + +void PDFTextToSpeech::updatePlay() +{ + if (m_state != Playing) + { + return; + } + + Q_ASSERT(m_proxy); + Q_ASSERT(m_document); + + // Jakub Melka: Check, if we have text layout. If not, then create it and return immediately. + // Otherwise, check, if we have something to say. + pdf::PDFAsynchronousTextLayoutCompiler* compiler = m_proxy->getTextLayoutCompiler(); + if (!compiler->isTextLayoutReady()) + { + compiler->makeTextLayout(); + return; + } + + QTextToSpeech::State state = m_textToSpeech->state(); + if (state == QTextToSpeech::Ready) + { + if (m_currentPage == -1) + { + // Handle starting of document reading + std::vector currentPages = m_proxy->getWidget()->getDrawWidget()->getCurrentPages(); + if (!currentPages.empty()) + { + updateToNextPage(currentPages.front()); + } + } + else if (++m_currentTextFlowIndex >= m_textFlows.size()) + { + // Handle transition to next page + updateToNextPage(m_currentPage + 1); + } + + if (m_currentTextFlowIndex < m_textFlows.size()) + { + // Say next thing + const pdf::PDFTextFlow& textFlow = m_textFlows[m_currentTextFlowIndex]; + QString text = textFlow.getText(); + m_textToSpeech->say(text); + } + else + { + // We are finished the reading + m_state = Ready; + } + } + else if (state == QTextToSpeech::BackendError) + { + m_state = Error; + } + + updateUI(); +} + +void PDFTextToSpeech::updateToNextPage(pdf::PDFInteger pageIndex) +{ + Q_ASSERT(m_document); + Q_ASSERT(m_state = Playing); + + m_currentPage = pageIndex; + const pdf::PDFInteger pageCount = m_document->getCatalog()->getPageCount(); + + pdf::PDFAsynchronousTextLayoutCompiler* compiler = m_proxy->getTextLayoutCompiler(); + Q_ASSERT(compiler->isTextLayoutReady()); + + // Find first nonempty page + while (m_currentPage < pageCount) + { + m_currentTextLayout = compiler->getTextLayout(m_currentPage); + m_textFlows = pdf::PDFTextFlow::createTextFlows(m_currentTextLayout, pdf::PDFTextFlow::SeparateBlocks | pdf::PDFTextFlow::RemoveSoftHyphen, m_currentPage); + + if (!m_textFlows.empty()) + { + break; + } + + ++m_currentPage; + } + + m_currentTextFlowIndex = 0; +} + +} // namespace pdfviewer diff --git a/PdfForQtViewer/pdftexttospeech.h b/PdfForQtViewer/pdftexttospeech.h new file mode 100644 index 0000000..33b1475 --- /dev/null +++ b/PdfForQtViewer/pdftexttospeech.h @@ -0,0 +1,136 @@ +// Copyright (C) 2020 Jakub Melka +// +// This file is part of PdfForQt. +// +// PdfForQt 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 +// (at your option) any later version. +// +// PdfForQt 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 PDFForQt. If not, see . + + +#ifndef PDFTEXTTOSPEECH_H +#define PDFTEXTTOSPEECH_H + +#include "pdftextlayout.h" + +#include + +class QSlider; +class QComboBox; +class QToolButton; +class QTextToSpeech; + +namespace pdf +{ +class PDFDocument; +class PDFDrawWidgetProxy; +} + +namespace pdfviewer +{ +class PDFViewerSettings; + +/// Text to speech engine used to reading the document +class PDFTextToSpeech : public QObject +{ + Q_OBJECT + +private: + using BaseClass = QObject; + +public: + explicit PDFTextToSpeech(QObject* parent); + + enum State + { + Invalid, ///< Text to speech engine is invalid (maybe bad engine) + NoDocument, ///< No document to read + Ready, ///< Ready to read the document contents + Playing, ///< Document is being read + Paused, ///< User paused the reading + Error ///< Error occured in text to speech engine + }; + + /// Returns true, if text to speech engine is valid and can be used + /// to synthetise text. + bool isValid() const; + + /// Sets active document to text to speech engine + void setDocument(const pdf::PDFDocument* document); + + /// Apply settings to the reader + void setSettings(const PDFViewerSettings* viewerSettings); + + /// Set draw proxy + void setProxy(const pdf::PDFDrawWidgetProxy* proxy); + + /// Initialize the ui, which is used + void initializeUI(QComboBox* speechLocaleComboBox, + QComboBox* speechVoiceComboBox, + QSlider* speechRateEdit, + QSlider* speechVolumeEdit, + QSlider* speechPitchEdit, + QToolButton* speechPlayButton, + QToolButton* speechPauseButton, + QToolButton* speechStopButton, + QToolButton* speechSynchronizeButton); + +private: + /// Updates UI controls depending on the state + void updateUI(); + + /// Stop the engine, if it is reading + void stop(); + + void setLocale(const QString& locale); + void setVoice(const QString& voice); + void setRate(const double rate); + void setPitch(const double pitch); + void setVolume(const double volume); + + void onLocaleChanged(); + void onVoiceChanged(); + void onRateChanged(int rate); + void onPitchChanged(int pitch); + void onVolumeChanged(int volume); + + void onPlayClicked(); + void onPauseClicked(); + void onStopClicked(); + + void updatePlay(); + void updateToNextPage(pdf::PDFInteger pageIndex); + + QTextToSpeech* m_textToSpeech; + const pdf::PDFDocument* m_document; + const pdf::PDFDrawWidgetProxy* m_proxy; + State m_state; + bool m_initialized; + + QComboBox* m_speechLocaleComboBox; + QComboBox* m_speechVoiceComboBox; + QSlider* m_speechRateEdit; + QSlider* m_speechVolumeEdit; + QSlider* m_speechPitchEdit; + QToolButton* m_speechPlayButton; + QToolButton* m_speechPauseButton; + QToolButton* m_speechStopButton; + QToolButton* m_speechSynchronizeButton; + + pdf::PDFTextLayout m_currentTextLayout; ///< Text layout for actual page + pdf::PDFTextFlows m_textFlows; ///< Text flows for actual page + size_t m_currentTextFlowIndex = 0; ///< Index of current text flow + pdf::PDFInteger m_currentPage = 0; ///< Current page +}; + +} // namespace pdfviewer + +#endif // PDFTEXTTOSPEECH_H diff --git a/PdfForQtViewer/pdfviewermainwindow.cpp b/PdfForQtViewer/pdfviewermainwindow.cpp index 764ab45..7954673 100644 --- a/PdfForQtViewer/pdfviewermainwindow.cpp +++ b/PdfForQtViewer/pdfviewermainwindow.cpp @@ -88,7 +88,8 @@ PDFViewerMainWindow::PDFViewerMainWindow(QWidget* parent) : m_progressDialog(nullptr), m_isBusy(false), m_isChangingProgressStep(false), - m_toolManager(nullptr) + m_toolManager(nullptr), + m_textToSpeech(new PDFTextToSpeech(this)) { ui->setupUi(this); @@ -199,8 +200,9 @@ PDFViewerMainWindow::PDFViewerMainWindow(QWidget* parent) : setFocusProxy(m_pdfWidget); m_pdfWidget->updateCacheLimits(m_settings->getCompiledPageCacheLimit() * 1024, m_settings->getThumbnailsCacheLimit(), m_settings->getFontCacheLimit(), m_settings->getInstancedFontCacheLimit()); m_pdfWidget->getDrawWidgetProxy()->setProgress(m_progress); + m_textToSpeech->setProxy(m_pdfWidget->getDrawWidgetProxy()); - m_sidebarWidget = new PDFSidebarWidget(m_pdfWidget->getDrawWidgetProxy(), this); + m_sidebarWidget = new PDFSidebarWidget(m_pdfWidget->getDrawWidgetProxy(), m_textToSpeech, this); m_sidebarDockWidget = new QDockWidget(tr("Sidebar"), this); m_sidebarDockWidget->setObjectName("SidebarDockWidget"); m_sidebarDockWidget->setAllowedAreas(Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea); @@ -208,6 +210,7 @@ PDFViewerMainWindow::PDFViewerMainWindow(QWidget* parent) : addDockWidget(Qt::LeftDockWidgetArea, m_sidebarDockWidget); m_sidebarDockWidget->hide(); connect(m_sidebarWidget, &PDFSidebarWidget::actionTriggered, this, &PDFViewerMainWindow::onActionTriggered); + m_textToSpeech->setSettings(m_settings); m_advancedFindWidget = new PDFAdvancedFindWidget(m_pdfWidget->getDrawWidgetProxy(), this); m_advancedFindDockWidget = new QDockWidget(tr("Advanced find"), this); @@ -648,6 +651,7 @@ void PDFViewerMainWindow::readSettings() m_settings->readSettings(settings, m_CMSManager->getDefaultSettings()); m_CMSManager->setSettings(m_settings->getColorManagementSystemSettings()); + m_textToSpeech->setSettings(m_settings); } void PDFViewerMainWindow::readActionSettings() @@ -956,6 +960,7 @@ void PDFViewerMainWindow::setDocument(const pdf::PDFDocument* document) } m_toolManager->setDocument(document); + m_textToSpeech->setDocument(document); m_pdfWidget->setDocument(document, m_optionalContentActivity); m_sidebarWidget->setDocument(document, m_optionalContentActivity); m_advancedFindWidget->setDocument(document); @@ -1130,6 +1135,7 @@ void PDFViewerMainWindow::on_actionOptions_triggered() m_settings->setColorManagementSystemSettings(dialog.getCMSSettings()); m_CMSManager->setSettings(m_settings->getColorManagementSystemSettings()); m_recentFileManager->setRecentFilesLimit(dialog.getOtherSettings().maximumRecentFileCount); + m_textToSpeech->setSettings(m_settings); } } diff --git a/PdfForQtViewer/pdfviewermainwindow.h b/PdfForQtViewer/pdfviewermainwindow.h index 7918147..0635330 100644 --- a/PdfForQtViewer/pdfviewermainwindow.h +++ b/PdfForQtViewer/pdfviewermainwindow.h @@ -27,6 +27,7 @@ #include "pdfdocumentpropertiesdialog.h" #include "pdfwidgettool.h" #include "pdfrecentfilemanager.h" +#include "pdftexttospeech.h" #include #include @@ -177,6 +178,7 @@ private: bool m_isChangingProgressStep; pdf::PDFToolManager* m_toolManager; + PDFTextToSpeech* m_textToSpeech; }; } // namespace pdfviewer diff --git a/PdfForQtViewer/resources/pause.svg b/PdfForQtViewer/resources/pause.svg new file mode 100644 index 0000000..2943ef0 --- /dev/null +++ b/PdfForQtViewer/resources/pause.svg @@ -0,0 +1,93 @@ + + + + + + + + + + image/svg+xml + + + + + Jakub Melka + + + + + + + + + + + + + + + + + + + diff --git a/PdfForQtViewer/resources/play.svg b/PdfForQtViewer/resources/play.svg new file mode 100644 index 0000000..f49a009 --- /dev/null +++ b/PdfForQtViewer/resources/play.svg @@ -0,0 +1,84 @@ + + + + + + + + + + image/svg+xml + + + + + Jakub Melka + + + + + + + + + + + + + + + + + + diff --git a/PdfForQtViewer/resources/stop.svg b/PdfForQtViewer/resources/stop.svg new file mode 100644 index 0000000..7a31412 --- /dev/null +++ b/PdfForQtViewer/resources/stop.svg @@ -0,0 +1,93 @@ + + + + + + + + + + image/svg+xml + + + + + Jakub Melka + + + + + + + + + + + + + + + + + + +