diff --git a/Pdf4QtLib/sources/pdfdocumenttextflow.cpp b/Pdf4QtLib/sources/pdfdocumenttextflow.cpp
index 41f0025..01e2ce5 100644
--- a/Pdf4QtLib/sources/pdfdocumenttextflow.cpp
+++ b/Pdf4QtLib/sources/pdfdocumenttextflow.cpp
@@ -974,6 +974,27 @@ PDFDocumentTextFlowEditor::PageIndicesMappingRange PDFDocumentTextFlowEditor::ge
return std::equal_range(m_pageIndicesMapping.cbegin(), m_pageIndicesMapping.cend(), std::make_pair(pageIndex, size_t(0)), comparator);
}
+PDFDocumentTextFlow PDFDocumentTextFlowEditor::createEditedTextFlow() const
+{
+ PDFDocumentTextFlow::Items items;
+ items.reserve(getItemCount());
+
+ const size_t size = getItemCount();
+ for (size_t i = 0; i < size; ++i)
+ {
+ if (isRemoved(i))
+ {
+ continue;
+ }
+
+ PDFDocumentTextFlow::Item item = *getOriginalItem(i);
+ item.text = getText(i);
+ items.emplace_back(std::move(item));
+ }
+
+ return PDFDocumentTextFlow(std::move(items));
+}
+
void PDFDocumentTextFlowEditor::createPageMapping()
{
m_pageIndicesMapping.clear();
diff --git a/Pdf4QtLib/sources/pdfdocumenttextflow.h b/Pdf4QtLib/sources/pdfdocumenttextflow.h
index b139a89..98be760 100644
--- a/Pdf4QtLib/sources/pdfdocumenttextflow.h
+++ b/Pdf4QtLib/sources/pdfdocumenttextflow.h
@@ -261,6 +261,11 @@ public:
const EditedItem* getEditedItem(size_t index) const { return &m_editedTextFlow.at(index); }
+ /// Creates text flow from active edited items. If item is removed,
+ /// then it is not added into this text flow. User text modification
+ /// is applied to a text flow.
+ PDFDocumentTextFlow createEditedTextFlow() const;
+
private:
void createPageMapping();
void createEditedFromOriginalTextFlow();
diff --git a/Pdf4QtLib/sources/pdfplugin.h b/Pdf4QtLib/sources/pdfplugin.h
index a9f9cdc..2c1a8fb 100644
--- a/Pdf4QtLib/sources/pdfplugin.h
+++ b/Pdf4QtLib/sources/pdfplugin.h
@@ -54,9 +54,19 @@ public:
explicit IPluginDataExchange() = default;
virtual ~IPluginDataExchange() = default;
+ struct VoiceSettings
+ {
+ QString directory;
+ QString voiceName;
+ double volume = 1.0;
+ double rate = 0.0;
+ double pitch = 0.0;
+ };
+
virtual QString getOriginalFileName() const = 0;
virtual pdf::PDFTextSelection getSelectedText() const = 0;
virtual QMainWindow* getMainWindow() const = 0;
+ virtual VoiceSettings getVoiceSettings() const = 0;
};
class PDF4QTLIBSHARED_EXPORT PDFPlugin : public QObject
diff --git a/Pdf4QtViewer/Pdf4QtViewer.pro b/Pdf4QtViewer/Pdf4QtViewer.pro
index 23074f9..b10a346 100644
--- a/Pdf4QtViewer/Pdf4QtViewer.pro
+++ b/Pdf4QtViewer/Pdf4QtViewer.pro
@@ -99,7 +99,8 @@ plugins.files = $$DESTDIR/pdfplugins/ObjectInspectorPlugin.dll \
$$DESTDIR/pdfplugins/OutputPreviewPlugin.dll \
$$DESTDIR/pdfplugins/DimensionsPlugin.dll \
$$DESTDIR/pdfplugins/SoftProofingPlugin.dll \
- $$DESTDIR/pdfplugins/RedactPlugin.dll
+ $$DESTDIR/pdfplugins/RedactPlugin.dll \
+ $$DESTDIR/pdfplugins/AudioBookPlugin.dll
plugins.path = $$DESTDIR/install/pdfplugins
plugins.CONFIG += no_check_exist
diff --git a/Pdf4QtViewer/pdfprogramcontroller.cpp b/Pdf4QtViewer/pdfprogramcontroller.cpp
index f4702ba..e4f6c2f 100644
--- a/Pdf4QtViewer/pdfprogramcontroller.cpp
+++ b/Pdf4QtViewer/pdfprogramcontroller.cpp
@@ -1078,6 +1078,20 @@ QMainWindow* PDFProgramController::getMainWindow() const
return m_mainWindow;
}
+pdf::IPluginDataExchange::VoiceSettings PDFProgramController::getVoiceSettings() const
+{
+ VoiceSettings voiceSettings;
+
+ const PDFViewerSettings::Settings& settings = m_settings->getSettings();
+ voiceSettings.directory = m_settings->getDirectory();
+ voiceSettings.voiceName = settings.m_speechVoice;
+ voiceSettings.pitch = settings.m_speechPitch;
+ voiceSettings.rate = settings.m_speechRate;
+ voiceSettings.volume = settings.m_speechVolume;
+
+ return voiceSettings;
+}
+
void PDFProgramController::onActionRotateRightTriggered()
{
m_pdfWidget->getDrawWidgetProxy()->performOperation(pdf::PDFDrawWidgetProxy::RotateRight);
diff --git a/Pdf4QtViewer/pdfprogramcontroller.h b/Pdf4QtViewer/pdfprogramcontroller.h
index eeebb17..bbd8e27 100644
--- a/Pdf4QtViewer/pdfprogramcontroller.h
+++ b/Pdf4QtViewer/pdfprogramcontroller.h
@@ -286,6 +286,7 @@ public:
virtual QString getOriginalFileName() const override;
virtual pdf::PDFTextSelection getSelectedText() const override;
virtual QMainWindow* getMainWindow() const override;
+ virtual VoiceSettings getVoiceSettings() const override;
signals:
void queryPasswordRequest(QString* password, bool* ok);
diff --git a/Pdf4QtViewerPlugins/AudioBookPlugin/AudioBookPlugin.pro b/Pdf4QtViewerPlugins/AudioBookPlugin/AudioBookPlugin.pro
index 37cf0a6..536347f 100644
--- a/Pdf4QtViewerPlugins/AudioBookPlugin/AudioBookPlugin.pro
+++ b/Pdf4QtViewerPlugins/AudioBookPlugin/AudioBookPlugin.pro
@@ -33,10 +33,12 @@ DESTDIR = $$OUT_PWD/../../pdfplugins
CONFIG += c++11
SOURCES += \
+ audiobookcreator.cpp \
audiobookplugin.cpp \
audiotextstreameditordockwidget.cpp
HEADERS += \
+ audiobookcreator.h \
audiobookplugin.h \
audiotextstreameditordockwidget.h
diff --git a/Pdf4QtViewerPlugins/AudioBookPlugin/audiobookcreator.cpp b/Pdf4QtViewerPlugins/AudioBookPlugin/audiobookcreator.cpp
new file mode 100644
index 0000000..74e3fb8
--- /dev/null
+++ b/Pdf4QtViewerPlugins/AudioBookPlugin/audiobookcreator.cpp
@@ -0,0 +1,176 @@
+// Copyright (C) 2021 Jakub Melka
+//
+// This file is part of PDF4QT.
+//
+// PDF4QT 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
+// with the written consent of the copyright owner, any later version.
+//
+// PDF4QT 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 PDF4QT. If not, see .
+
+#include "audiobookcreator.h"
+
+#ifdef Q_OS_WIN
+#include
+#pragma comment(lib, "ole32")
+#endif
+
+namespace pdfplugin
+{
+
+AudioBookCreator::AudioBookCreator() :
+ m_initialized(false)
+{
+#ifdef Q_OS_WIN
+ auto comResult = ::CoInitialize(nullptr);
+ m_initialized = SUCCEEDED(comResult);
+#endif
+}
+
+AudioBookCreator::~AudioBookCreator()
+{
+#ifdef Q_OS_WIN
+ if (m_initialized)
+ {
+ ::CoUninitialize();
+ }
+#endif
+}
+
+pdf::PDFOperationResult AudioBookCreator::createAudioBook(const Settings& settings, pdf::PDFDocumentTextFlow& flow)
+{
+#ifdef Q_OS_WIN
+ QString audioString;
+ QTextStream textStream(&audioString);
+
+ for (const pdf::PDFDocumentTextFlow::Item& item : flow.getItems())
+ {
+ QString trimmedText = item.text.trimmed();
+ if (!trimmedText.isEmpty())
+ {
+ textStream << trimmedText << endl;
+ }
+ }
+
+ auto getVoiceToken = [](const Settings& settings)
+ {
+ ISpObjectToken* token = nullptr;
+
+ QStringList voiceSelector;
+ if (!settings.voiceName.isEmpty())
+ {
+ voiceSelector << QString("Name=%1").arg(settings.voiceName);
+ }
+ if (!settings.voiceGender.isEmpty())
+ {
+ voiceSelector << QString("Gender=%1").arg(settings.voiceGender);
+ }
+ if (!settings.voiceAge.isEmpty())
+ {
+ voiceSelector << QString("Age=%1").arg(settings.voiceAge);
+ }
+ if (!settings.voiceLangCode.isEmpty())
+ {
+ voiceSelector << QString("Language=%1").arg(settings.voiceLangCode);
+ }
+ QString voiceSelectorString = voiceSelector.join(";");
+ LPCWSTR requiredAttributes = !voiceSelectorString.isEmpty() ? (LPCWSTR)voiceSelectorString.utf16() : nullptr;
+
+ ISpObjectTokenCategory* category = nullptr;
+ if (!SUCCEEDED(::CoCreateInstance(CLSID_SpObjectTokenCategory, NULL, CLSCTX_ALL, __uuidof(ISpObjectTokenCategory), (LPVOID*)&category)))
+ {
+ return token;
+ }
+
+ if (!SUCCEEDED(category->SetId(SPCAT_VOICES, FALSE)))
+ {
+ category->Release();
+ return token;
+ }
+
+ IEnumSpObjectTokens* enumTokensObject = nullptr;
+ if (SUCCEEDED(category->EnumTokens(requiredAttributes, NULL, &enumTokensObject)))
+ {
+ enumTokensObject->Next(1, &token, NULL);
+ }
+
+ if (enumTokensObject)
+ {
+ enumTokensObject->Release();
+ }
+
+ if (category)
+ {
+ category->Release();
+ }
+
+ return token;
+ };
+
+ ISpObjectToken* voiceToken = getVoiceToken(settings);
+
+ // Do we have any voice?
+ if (!voiceToken)
+ {
+ return tr("No suitable voice found.");
+ }
+
+ QString outputFile = settings.audioFileName;
+ BSTR outputFileName = (BSTR)outputFile.utf16();
+
+ ISpeechFileStream* stream = nullptr;
+ if (!SUCCEEDED(::CoCreateInstance(CLSID_SpFileStream, NULL, CLSCTX_ALL, __uuidof(ISpeechFileStream), (LPVOID*)&stream)))
+ {
+ voiceToken->Release();
+ return tr("Cannot create output stream '%1'.").arg(outputFile);
+ }
+
+ ISpVoice* voice = nullptr;
+ if (!SUCCEEDED(::CoCreateInstance(CLSID_SpVoice, NULL, CLSCTX_ALL, __uuidof(ISpVoice), (LPVOID*)&voice)))
+ {
+ voiceToken->Release();
+ stream->Release();
+ return tr("Cannot create voice.");
+ }
+
+ if (!SUCCEEDED(stream->Open(outputFileName, SSFMCreateForWrite)))
+ {
+ voiceToken->Release();
+ voice->Release();
+ stream->Release();
+ return tr("Cannot create output stream '%1'.").arg(outputFile);
+ }
+
+ if (!SUCCEEDED(voice->SetVoice(voiceToken)))
+ {
+ voiceToken->Release();
+ voice->Release();
+ stream->Release();
+ return tr("Failed to set requested voice.");
+ }
+
+ LPCWSTR stringToSpeak = (LPCWSTR)audioString.utf16();
+
+ voice->SetOutput(stream, FALSE);
+ voice->SetRate(settings.rate * 10.0);
+ voice->SetVolume(settings.volume * 100.0);
+ voice->Speak(stringToSpeak, SPF_PURGEBEFORESPEAK | SPF_PARSE_SAPI, NULL);
+
+ voice->Release();
+ stream->Release();
+ voiceToken->Release();
+
+ return true;
+#else
+ return tr("Audio book plugin is unsupported on your system.");
+#endif
+}
+
+} // namespace pdfplugin
diff --git a/Pdf4QtViewerPlugins/AudioBookPlugin/audiobookcreator.h b/Pdf4QtViewerPlugins/AudioBookPlugin/audiobookcreator.h
new file mode 100644
index 0000000..4d35de7
--- /dev/null
+++ b/Pdf4QtViewerPlugins/AudioBookPlugin/audiobookcreator.h
@@ -0,0 +1,57 @@
+// Copyright (C) 2021 Jakub Melka
+//
+// This file is part of PDF4QT.
+//
+// PDF4QT 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
+// with the written consent of the copyright owner, any later version.
+//
+// PDF4QT 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 PDF4QT. If not, see .
+
+#ifndef AUDIOBOOKCREATOR_H
+#define AUDIOBOOKCREATOR_H
+
+#include "pdfdocumenttextflow.h"
+
+#include
+
+namespace pdfplugin
+{
+
+class AudioBookCreator
+{
+ Q_DECLARE_TR_FUNCTIONS(pdfplugin::AudioBookCreator)
+
+public:
+ AudioBookCreator();
+ ~AudioBookCreator();
+
+ struct Settings
+ {
+ QString audioFileName;
+ QString voiceName;
+ QString voiceGender;
+ QString voiceAge;
+ QString voiceLangCode;
+ double rate = 0.0;
+ double volume = 1.0;
+ };
+
+ bool isInitialized() const { return m_initialized; }
+
+ pdf::PDFOperationResult createAudioBook(const Settings& settings, pdf::PDFDocumentTextFlow& flow);
+
+private:
+ bool m_initialized;
+};
+
+} // namespace pdfplugin
+
+#endif // AUDIOBOOKCREATOR_H
diff --git a/Pdf4QtViewerPlugins/AudioBookPlugin/audiobookplugin.cpp b/Pdf4QtViewerPlugins/AudioBookPlugin/audiobookplugin.cpp
index 68bbf60..8866b1d 100644
--- a/Pdf4QtViewerPlugins/AudioBookPlugin/audiobookplugin.cpp
+++ b/Pdf4QtViewerPlugins/AudioBookPlugin/audiobookplugin.cpp
@@ -20,6 +20,7 @@
#include "pdfwidgettool.h"
#include "pdfutils.h"
#include "pdfwidgetutils.h"
+#include "audiobookcreator.h"
#include
#include
@@ -27,6 +28,7 @@
#include
#include
#include
+#include
#include
namespace pdfplugin
@@ -117,7 +119,7 @@ void AudioBookPlugin::setWidget(pdf::PDFWidget* widget)
m_actionCreateAudioBook = new QAction(QIcon(":/pdfplugins/audiobook/create-audio-book.svg"), tr("Create Audio Book"), this);
m_actionCreateAudioBook->setObjectName("actionAudioBook_CreateAudioBook");
- connect(m_actionRestoreOriginalText, &QAction::triggered, this, &AudioBookPlugin::onCreateAudioBook);
+ connect(m_actionCreateAudioBook, &QAction::triggered, this, &AudioBookPlugin::onCreateAudioBook);
m_actionClear = new QAction(QIcon(":/pdfplugins/audiobook/clear.svg"), tr("Clear Text Stream"), this);
m_actionClear->setObjectName("actionAudioBook_Clear");
@@ -425,7 +427,35 @@ void AudioBookPlugin::onMoveSelectionDown()
void AudioBookPlugin::onCreateAudioBook()
{
+ pdf::IPluginDataExchange::VoiceSettings voiceSettings = m_dataExchangeInterface->getVoiceSettings();
+ QString fileName = QFileDialog::getSaveFileName(m_widget, tr("Select Audio File"), voiceSettings.directory, tr("Audio stream (*.mp3)"));
+ if (fileName.isEmpty())
+ {
+ return;
+ }
+
+ pdf::PDFOperationResult result = true;
+ AudioBookCreator audioBookCreator;
+ if (audioBookCreator.isInitialized())
+ {
+ AudioBookCreator::Settings settings;
+ settings.audioFileName = fileName;
+ settings.voiceName = voiceSettings.voiceName;
+ settings.rate = voiceSettings.rate;
+ settings.volume = voiceSettings.volume;
+ pdf::PDFDocumentTextFlow textFlow = m_textFlowEditor.createEditedTextFlow();
+ result = audioBookCreator.createAudioBook(settings, textFlow);
+ }
+ else
+ {
+ result = tr("Audio book creator cannot be initialized.");
+ }
+
+ if (!result)
+ {
+ QMessageBox::critical(m_widget, tr("Error"), result.getErrorMessage());
+ }
}
void AudioBookPlugin::onRectanglePicked(pdf::PDFInteger pageIndex, QRectF rectangle)
diff --git a/PdfTool/pdftoolaudiobook.cpp b/PdfTool/pdftoolaudiobook.cpp
index 9543839..2ee10a6 100644
--- a/PdfTool/pdftoolaudiobook.cpp
+++ b/PdfTool/pdftoolaudiobook.cpp
@@ -132,6 +132,7 @@ int PDFToolAudioBookBase::fillVoices(const PDFToolOptions& options, PDFVoiceInfo
if (!SUCCEEDED(category->SetId(SPCAT_VOICES, FALSE)))
{
PDFConsole::writeError(PDFToolTranslationContext::tr("SAPI Error: Cannot enumerate SAPI voices."), options.outputCodec);
+ category->Release();
return ErrorSAPI;
}