// 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 "audiobookplugin.h" #include "pdfdrawwidget.h" #include "pdfwidgettool.h" #include "pdfutils.h" #include "pdfwidgetutils.h" #include "audiobookcreator.h" #include #include #include #include #include #include #include #include namespace pdfplugin { AudioBookPlugin::AudioBookPlugin() : pdf::PDFPlugin(nullptr), m_actionCreateTextStream(nullptr), m_actionSynchronizeFromTableToGraphics(nullptr), m_actionSynchronizeFromGraphicsToTable(nullptr), m_actionActivateSelection(nullptr), m_actionDeactivateSelection(nullptr), m_actionSelectByRectangle(nullptr), m_actionSelectByContainedText(nullptr), m_actionSelectByRegularExpression(nullptr), m_actionSelectByPageList(nullptr), m_actionRestoreOriginalText(nullptr), m_actionMoveSelectionUp(nullptr), m_actionMoveSelectionDown(nullptr), m_actionCreateAudioBook(nullptr), m_actionClear(nullptr), m_audioTextStreamDockWidget(nullptr), m_audioTextStreamEditorModel(nullptr) { } AudioBookPlugin::~AudioBookPlugin() { } void AudioBookPlugin::setWidget(pdf::PDFWidget* widget) { Q_ASSERT(!m_widget); BaseClass::setWidget(widget); m_actionCreateTextStream = new QAction(QIcon(":/pdfplugins/audiobook/create-text-stream.svg"), tr("Create Text Stream for Audio Book"), this); m_actionCreateTextStream->setObjectName("actionAudioBook_CreateTextStream"); connect(m_actionCreateTextStream, &QAction::triggered, this, &AudioBookPlugin::onCreateTextStreamTriggered); m_actionSynchronizeFromTableToGraphics = new QAction(QIcon(":/pdfplugins/audiobook/synchronize-from-table-to-graphics.svg"), tr("Synchronize Selection from Table to Graphics"), this); m_actionSynchronizeFromTableToGraphics->setObjectName("actionAudioBook_SynchronizeFromTableToGraphics"); m_actionSynchronizeFromTableToGraphics->setCheckable(true); m_actionSynchronizeFromTableToGraphics->setChecked(true); m_actionSynchronizeFromGraphicsToTable = new QAction(QIcon(":/pdfplugins/audiobook/synchronize-from-graphics-to-table.svg"), tr("Synchronize Selection from Graphics to Table"), this); m_actionSynchronizeFromGraphicsToTable->setObjectName("actionAudioBook_SynchronizeFromGraphicsToTable"); m_actionSynchronizeFromGraphicsToTable->setCheckable(true); m_actionSynchronizeFromGraphicsToTable->setChecked(true); m_actionActivateSelection = new QAction(QIcon(":/pdfplugins/audiobook/activate-selection.svg"), tr("Activate Selection"), this); m_actionActivateSelection->setObjectName("actionAudioBook_ActivateSelection"); connect(m_actionActivateSelection, &QAction::triggered, this, &AudioBookPlugin::onActivateSelection); m_actionDeactivateSelection = new QAction(QIcon(":/pdfplugins/audiobook/deactivate-selection.svg"), tr("Deactivate Selection"), this); m_actionDeactivateSelection->setObjectName("actionAudioBook_DeactivateSelection"); connect(m_actionDeactivateSelection, &QAction::triggered, this, &AudioBookPlugin::onDeactivateSelection); m_actionSelectByRectangle = new QAction(QIcon(":/pdfplugins/audiobook/select-by-rectangle.svg"), tr("Select by Rectangle"), this); m_actionSelectByRectangle->setObjectName("actionAudioBook_SelectByRectangle"); connect(m_actionSelectByRectangle, &QAction::triggered, this, &AudioBookPlugin::onSelectByRectangle); m_actionSelectByContainedText = new QAction(QIcon(":/pdfplugins/audiobook/select-by-contained-text.svg"), tr("Select by Contained Text"), this); m_actionSelectByContainedText->setObjectName("actionAudioBook_SelectByContainedText"); connect(m_actionSelectByContainedText, &QAction::triggered, this, &AudioBookPlugin::onSelectByContainedText); m_actionSelectByRegularExpression = new QAction(QIcon(":/pdfplugins/audiobook/select-by-regular-expression.svg"), tr("Select by Regular Expression"), this); m_actionSelectByRegularExpression->setObjectName("actionAudioBook_SelectByRegularExpression"); connect(m_actionSelectByRegularExpression, &QAction::triggered, this, &AudioBookPlugin::onSelectByRegularExpression); m_actionSelectByPageList = new QAction(QIcon(":/pdfplugins/audiobook/select-by-page-list.svg"), tr("Select by Page List"), this); m_actionSelectByPageList->setObjectName("actionAudioBook_SelectByPageList"); connect(m_actionSelectByPageList, &QAction::triggered, this, &AudioBookPlugin::onSelectByPageList); m_actionRestoreOriginalText = new QAction(QIcon(":/pdfplugins/audiobook/restore-original-text.svg"), tr("Restore Original Text"), this); m_actionRestoreOriginalText->setObjectName("actionAudioBook_RestoreOriginalText"); connect(m_actionRestoreOriginalText, &QAction::triggered, this, &AudioBookPlugin::onRestoreOriginalText); m_actionMoveSelectionUp = new QAction(QIcon(":/pdfplugins/audiobook/move-selection-up.svg"), tr("Move Selection Up"), this); m_actionMoveSelectionUp->setObjectName("actionAudioBook_MoveSelectionUp"); connect(m_actionMoveSelectionUp, &QAction::triggered, this, &AudioBookPlugin::onMoveSelectionUp); m_actionMoveSelectionDown = new QAction(QIcon(":/pdfplugins/audiobook/move-selection-down.svg"), tr("Move Selection Down"), this); m_actionMoveSelectionDown->setObjectName("actionAudioBook_MoveSelectionDown"); connect(m_actionMoveSelectionDown, &QAction::triggered, this, &AudioBookPlugin::onMoveSelectionDown); m_actionCreateAudioBook = new QAction(QIcon(":/pdfplugins/audiobook/create-audio-book.svg"), tr("Create Audio Book"), this); m_actionCreateAudioBook->setObjectName("actionAudioBook_CreateAudioBook"); 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"); connect(m_actionClear, &QAction::triggered, this, &AudioBookPlugin::onClear); m_widget->getDrawWidgetProxy()->registerDrawInterface(this); m_widget->addInputInterface(this); updateActions(); } void AudioBookPlugin::setDocument(const pdf::PDFModifiedDocument& document) { BaseClass::setDocument(document); if (document.hasReset()) { if (m_audioTextStreamEditorModel) { m_audioTextStreamEditorModel->beginFlowChange(); } m_textFlowEditor.clear(); if (m_audioTextStreamEditorModel) { m_audioTextStreamEditorModel->endFlowChange(); } updateActions(); } } std::vector AudioBookPlugin::getActions() const { return { m_actionCreateTextStream, m_actionSynchronizeFromTableToGraphics, m_actionSynchronizeFromGraphicsToTable, m_actionCreateAudioBook, m_actionClear }; } void AudioBookPlugin::drawPage(QPainter* painter, pdf::PDFInteger pageIndex, const pdf::PDFPrecompiledPage* compiledPage, pdf::PDFTextLayoutGetter& layoutGetter, const QTransform& pagePointToDevicePointMatrix, QList& errors) const { Q_UNUSED(compiledPage); Q_UNUSED(layoutGetter); Q_UNUSED(errors); const qreal width = pdf::PDFWidgetUtils::scaleDPI_x(painter->device(), 1.0); QPen pen; pen.setWidthF(width); auto range = m_textFlowEditor.getItemsForPageIndex(pageIndex); for (auto it = range.first; it != range.second; ++it) { const size_t itemIndex = it->second; const pdf::PDFDocumentTextFlowEditor::EditedItem* item = m_textFlowEditor.getEditedItem(itemIndex); QRectF boundingRect = item->boundingRect; QColor color(Qt::green); if (m_textFlowEditor.isSelected(itemIndex)) { color = Qt::yellow; } else if (m_textFlowEditor.isRemoved(itemIndex)) { color = Qt::red; } else if (m_textFlowEditor.isModified(itemIndex)) { color = QColor::fromRgb(0xFF, 0xA5, 0, 255); } QColor strokeColor = color; QColor fillColor = color; fillColor.setAlphaF(0.2f); pen.setColor(strokeColor); painter->setPen(pen); painter->setBrush(QBrush(fillColor)); QPainterPath path; path.addRect(boundingRect); path = pagePointToDevicePointMatrix.map(path); painter->drawPath(path); } } void AudioBookPlugin::onCreateTextStreamTriggered() { Q_ASSERT(m_document); if (!m_audioTextStreamDockWidget) { AudioTextStreamActions actions; actions.actionCreateTextStream = m_actionCreateTextStream; actions.actionSynchronizeFromTableToGraphics = m_actionSynchronizeFromTableToGraphics; actions.actionSynchronizeFromGraphicsToTable = m_actionSynchronizeFromGraphicsToTable; actions.actionActivateSelection = m_actionActivateSelection; actions.actionDeactivateSelection = m_actionDeactivateSelection; actions.actionSelectByRectangle = m_actionSelectByRectangle; actions.actionSelectByContainedText = m_actionSelectByContainedText; actions.actionSelectByRegularExpression = m_actionSelectByRegularExpression; actions.actionSelectByPageList = m_actionSelectByPageList; actions.actionRestoreOriginalText = m_actionRestoreOriginalText; actions.actionMoveSelectionUp = m_actionMoveSelectionUp; actions.actionMoveSelectionDown = m_actionMoveSelectionDown; actions.actionCreateAudioBook = m_actionCreateAudioBook; actions.actionClear = m_actionClear; m_audioTextStreamDockWidget = new AudioTextStreamEditorDockWidget(actions, m_dataExchangeInterface->getMainWindow()); m_audioTextStreamDockWidget->setAllowedAreas(Qt::TopDockWidgetArea | Qt::BottomDockWidgetArea); m_dataExchangeInterface->getMainWindow()->addDockWidget(Qt::BottomDockWidgetArea, m_audioTextStreamDockWidget, Qt::Horizontal); m_audioTextStreamDockWidget->setFloating(false); Q_ASSERT(!m_audioTextStreamEditorModel); m_audioTextStreamEditorModel = new pdf::PDFDocumentTextFlowEditorModel(m_audioTextStreamDockWidget); m_audioTextStreamEditorModel->setEditor(&m_textFlowEditor); m_audioTextStreamDockWidget->setModel(m_audioTextStreamEditorModel); connect(m_audioTextStreamDockWidget->getTextStreamView()->selectionModel(), &QItemSelectionModel::selectionChanged, this, &AudioBookPlugin::onTextStreamTableSelectionChanged); connect(m_audioTextStreamEditorModel, &pdf::PDFDocumentTextFlowEditorModel::modelReset, this, &AudioBookPlugin::onEditedTextFlowChanged); connect(m_audioTextStreamEditorModel, &pdf::PDFDocumentTextFlowEditorModel::dataChanged, this, &AudioBookPlugin::onEditedTextFlowChanged); } m_audioTextStreamDockWidget->show(); if (!m_textFlowEditor.isEmpty()) { return; } pdf::PDFDocumentTextFlowFactory factory; factory.setCalculateBoundingBoxes(true); pdf::PDFDocumentTextFlow textFlow = factory.create(m_document, pdf::PDFDocumentTextFlowFactory::Algorithm::Auto); m_audioTextStreamEditorModel->beginFlowChange(); m_textFlowEditor.setTextFlow(std::move(textFlow)); m_audioTextStreamEditorModel->endFlowChange(); } void AudioBookPlugin::onActivateSelection() { m_audioTextStreamEditorModel->setSelectionActivated(true); } void AudioBookPlugin::onDeactivateSelection() { m_audioTextStreamEditorModel->setSelectionActivated(false); } void AudioBookPlugin::onSelectByRectangle() { m_widget->getToolManager()->pickRectangle(std::bind(&AudioBookPlugin::onRectanglePicked, this, std::placeholders::_1, std::placeholders::_2)); } void AudioBookPlugin::onSelectByContainedText() { QString text = m_audioTextStreamDockWidget->getSelectionText(); if (!text.isEmpty()) { m_audioTextStreamDockWidget->clearSelectionText(); m_audioTextStreamEditorModel->selectByContainedText(text); } else { QMessageBox::critical(m_audioTextStreamDockWidget, tr("Error"), tr("Cannot select items by text, because text is empty.")); } } void AudioBookPlugin::onSelectByRegularExpression() { QString pattern = m_audioTextStreamDockWidget->getSelectionText(); if (!pattern.isEmpty()) { QRegularExpression expression(pattern); if (expression.isValid()) { m_audioTextStreamDockWidget->clearSelectionText(); m_audioTextStreamEditorModel->selectByRegularExpression(expression); } else { QMessageBox::critical(m_audioTextStreamDockWidget, tr("Error"), tr("Regular expression is not valid. %1").arg(expression.errorString())); } } else { QMessageBox::critical(m_audioTextStreamDockWidget, tr("Error"), tr("Cannot select items by regular expression, because regular expression definition is empty.")); } } void AudioBookPlugin::onSelectByPageList() { QString pageIndicesText = m_audioTextStreamDockWidget->getSelectionText(); if (!pageIndicesText.isEmpty()) { QString errorMessage; auto pageIndices = pdf::PDFClosedIntervalSet::parse(1, m_document->getCatalog()->getPageCount(), pageIndicesText, &errorMessage); if (errorMessage.isEmpty()) { m_audioTextStreamDockWidget->clearSelectionText(); m_audioTextStreamEditorModel->selectByPageIndices(pageIndices); } else { QMessageBox::critical(m_audioTextStreamDockWidget, tr("Error"), tr("Cannot select items by page indices, because page indices are invalid. %1").arg(errorMessage)); } } else { QMessageBox::critical(m_audioTextStreamDockWidget, tr("Error"), tr("Cannot select items by page indices, because page indices are empty.")); } } void AudioBookPlugin::onRestoreOriginalText() { if (!m_textFlowEditor.isSelectionModified()) { // Nothing to restore return; } if (QMessageBox::question(m_audioTextStreamDockWidget, tr("Question"), tr("Restore original texts in selected items? All changes will be lost."), QMessageBox::No, QMessageBox::Yes) == QMessageBox::Yes) { m_audioTextStreamEditorModel->restoreOriginalTexts(); } } void AudioBookPlugin::onEditedTextFlowChanged() { if (m_widget) { m_widget->getDrawWidget()->getWidget()->update(); } updateActions(); } void AudioBookPlugin::onTextStreamTableSelectionChanged() { QTableView* tableView = m_audioTextStreamDockWidget->getTextStreamView(); QModelIndexList indices = tableView->selectionModel()->selectedIndexes(); if (m_actionSynchronizeFromTableToGraphics->isChecked() && !indices.empty()) { // Jakub Melka: we will find first index, which has valid page number for (const QModelIndex& index : indices) { pdf::PDFInteger pageIndex = m_textFlowEditor.getPageIndex(index.row()); if (pageIndex >= 0) { m_widget->getDrawWidgetProxy()->goToPage(pageIndex); break; } } } m_textFlowEditor.deselect(); for (const QModelIndex& index : indices) { m_textFlowEditor.select(index.row(), true); } m_audioTextStreamEditorModel->notifyDataChanged(); } void AudioBookPlugin::onClear() { if (m_audioTextStreamEditorModel) { m_audioTextStreamEditorModel->clear(); } } void AudioBookPlugin::onMoveSelectionUp() { if (m_audioTextStreamEditorModel) { m_audioTextStreamEditorModel->moveSelectionUp(); m_audioTextStreamDockWidget->getTextStreamView()->clearSelection(); } } void AudioBookPlugin::onMoveSelectionDown() { if (m_audioTextStreamEditorModel) { m_audioTextStreamEditorModel->moveSelectionDown(); m_audioTextStreamDockWidget->getTextStreamView()->clearSelection(); } } 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) { Q_UNUSED(pageIndex); m_audioTextStreamEditorModel->selectByRectangle(rectangle); } void AudioBookPlugin::updateActions() { m_actionCreateTextStream->setEnabled(m_document); m_actionSynchronizeFromTableToGraphics->setEnabled(true); m_actionSynchronizeFromGraphicsToTable->setEnabled(true); m_actionActivateSelection->setEnabled(!m_textFlowEditor.isSelectionEmpty()); m_actionDeactivateSelection->setEnabled(!m_textFlowEditor.isSelectionEmpty()); m_actionSelectByRectangle->setEnabled(!m_textFlowEditor.isEmpty()); m_actionSelectByContainedText->setEnabled(!m_textFlowEditor.isEmpty()); m_actionSelectByRegularExpression->setEnabled(!m_textFlowEditor.isEmpty()); m_actionSelectByPageList->setEnabled(!m_textFlowEditor.isEmpty()); m_actionRestoreOriginalText->setEnabled(!m_textFlowEditor.isEmpty()); m_actionMoveSelectionUp->setEnabled(!m_textFlowEditor.isEmpty()); m_actionMoveSelectionDown->setEnabled(!m_textFlowEditor.isEmpty()); m_actionCreateAudioBook->setEnabled(!m_textFlowEditor.isEmpty()); m_actionClear->setEnabled(!m_textFlowEditor.isEmpty()); } std::optional AudioBookPlugin::getItemIndexForPagePoint(QPoint pos) const { QPointF pagePoint; pdf::PDFInteger pageIndex = m_widget->getDrawWidgetProxy()->getPageUnderPoint(pos, &pagePoint); pdf::PDFDocumentTextFlowEditor::PageIndicesMappingRange itemRange = m_textFlowEditor.getItemsForPageIndex(pageIndex); for (auto it = itemRange.first; it != itemRange.second; ++it) { const pdf::PDFDocumentTextFlowEditor::EditedItem* item = m_textFlowEditor.getEditedItem(it->second); if (item->boundingRect.contains(pagePoint)) { return it->second; } } return std::nullopt; } void AudioBookPlugin::shortcutOverrideEvent(QWidget* widget, QKeyEvent* event) { Q_UNUSED(widget); Q_UNUSED(event); } void AudioBookPlugin::keyPressEvent(QWidget* widget, QKeyEvent* event) { Q_UNUSED(widget); if (m_textFlowEditor.isEmpty()) { // Jakub Melka: do nothing, editor is empty return; } if (event->key() == Qt::Key_Delete) { m_audioTextStreamEditorModel->setSelectionActivated(event->modifiers().testFlag(Qt::ShiftModifier)); event->accept(); } } void AudioBookPlugin::keyReleaseEvent(QWidget* widget, QKeyEvent* event) { Q_UNUSED(widget); Q_UNUSED(event); } void AudioBookPlugin::mousePressEvent(QWidget* widget, QMouseEvent* event) { Q_UNUSED(widget); if (m_textFlowEditor.isEmpty()) { // Jakub Melka: do nothing, editor is empty return; } if (event->button() == Qt::LeftButton) { std::optional index = getItemIndexForPagePoint(event->pos()); if (index) { // Scroll to index, if we are synchronizing if (m_actionSynchronizeFromGraphicsToTable->isChecked() && m_audioTextStreamDockWidget) { m_audioTextStreamDockWidget->goToIndex(*index); } // Handle selection const bool add = event->modifiers() & Qt::ControlModifier; const bool remove = event->modifiers() & Qt::ShiftModifier; const bool deselect = !add && !remove; if (deselect) { m_textFlowEditor.deselect(); } m_textFlowEditor.select(*index, !remove); if (m_audioTextStreamEditorModel) { m_audioTextStreamEditorModel->notifyDataChanged(); } } } } void AudioBookPlugin::mouseDoubleClickEvent(QWidget* widget, QMouseEvent* event) { Q_UNUSED(widget); Q_UNUSED(event); } void AudioBookPlugin::mouseReleaseEvent(QWidget* widget, QMouseEvent* event) { Q_UNUSED(widget); Q_UNUSED(event); } void AudioBookPlugin::mouseMoveEvent(QWidget* widget, QMouseEvent* event) { Q_UNUSED(widget); if (m_textFlowEditor.isEmpty()) { // Jakub Melka: do nothing, editor is empty return; } std::optional index = getItemIndexForPagePoint(event->pos()); if (index) { m_toolTip = m_textFlowEditor.getText(*index); } else { m_toolTip = QString(); } } void AudioBookPlugin::wheelEvent(QWidget* widget, QWheelEvent* event) { Q_UNUSED(widget); Q_UNUSED(event); } QString AudioBookPlugin::getTooltip() const { return m_toolTip; } const std::optional& AudioBookPlugin::getCursor() const { return m_cursor; } int AudioBookPlugin::getInputPriority() const { return UserPriority; } } // namespace pdfplugin