mirror of https://github.com/JakubMelka/PDF4QT.git
AudioBook Plugin: create audio book stream
This commit is contained in:
parent
3318a2a6d7
commit
ae4296776f
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
#include "audiobookcreator.h"
|
||||
|
||||
#ifdef Q_OS_WIN
|
||||
#include <sapi.h>
|
||||
#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
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
#ifndef AUDIOBOOKCREATOR_H
|
||||
#define AUDIOBOOKCREATOR_H
|
||||
|
||||
#include "pdfdocumenttextflow.h"
|
||||
|
||||
#include <QtGlobal>
|
||||
|
||||
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
|
|
@ -20,6 +20,7 @@
|
|||
#include "pdfwidgettool.h"
|
||||
#include "pdfutils.h"
|
||||
#include "pdfwidgetutils.h"
|
||||
#include "audiobookcreator.h"
|
||||
|
||||
#include <QAction>
|
||||
#include <QPainter>
|
||||
|
@ -27,6 +28,7 @@
|
|||
#include <QMessageBox>
|
||||
#include <QMouseEvent>
|
||||
#include <QTableView>
|
||||
#include <QFileDialog>
|
||||
#include <QRegularExpression>
|
||||
|
||||
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)
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue