diff --git a/src/corelib/business_layer/import/abstract_qtextdocument_importer.cpp b/src/corelib/business_layer/import/abstract_qtextdocument_importer.cpp new file mode 100644 index 000000000..c9d319aae --- /dev/null +++ b/src/corelib/business_layer/import/abstract_qtextdocument_importer.cpp @@ -0,0 +1,437 @@ +#include "abstract_qtextdocument_importer.h" + +#include "import_options.h" + +#include +#include +#include + +#include +#include +#include + + +namespace BusinessLayer { + +namespace { + +/** + * @brief Регулярное выражение для определения блока "Время и место" по наличию слов места + */ +const QRegularExpression kPlaceContainsChecker( + "^(INT|EXT|INT/EXT|ИНТ|НАТ|ИНТ/НАТ|ПАВ|ЭКСТ|ИНТ/ЭКСТ)([.]|[ - ])"); + +/** + * @brief Регулярное выражение для определения блока "Титр" по наличию ключевых слов + */ +const QRegularExpression kTitleChecker("(^|[^\\S])(TITLE|ТИТР)([:] )"); + +/** + * @brief Регулярное выражение для определения текста в скобках + */ +const QRegularExpression kTextInParenthesisChecker(".{1,}\\([^\\)]{1,}\\)"); + +/** + * @brief Допущение для блоков, которые по идее вообще не должны иметь отступа в пикселях (16 мм) + */ +const int kLeftMarginDelta = 60; + +/** + * @brief Некоторые программы выравнивают текст при помощи пробелов + */ +const QString kOldSchoolCenteringPrefix = " "; + +/** + * @brief Шум, который может встречаться в тексте + */ +const QString NOISE("([.]|[,]|[:]|[ ]|[-]){1,}"); + +/** + * @brief Регулярное выражение для удаления мусора в начале текста + */ +const QRegularExpression NOISE_AT_START("^" + NOISE); + +/** + * @brief Регулярное выражение для удаления мусора в конце текста + */ +const QRegularExpression NOISE_AT_END(NOISE + "$"); + +} // namespace + + +AbstractQTextDocumentImporter::AbstractQTextDocumentImporter() +{ +} + +AbstractQTextDocumentImporter::~AbstractQTextDocumentImporter() = default; + + +QString AbstractQTextDocumentImporter::clearBlockText(TextParagraphType _blockType, + const QString& _blockText) const +{ + QString result = _blockText; + + // + // Удаляем длинные тире + // + result = result.replace("–", "-"); + + // + // Для блока заголовка сцены: + // * всевозможные "инт - " меняем на "инт. " + // * убираем точки в конце названия локации + // + if (_blockType == TextParagraphType::SceneHeading) { + const QString location = /*ScreenplaySceneHeadingParser::location*/ (_blockText); + QString clearLocation = location.simplified(); + clearLocation.remove(NOISE_AT_START); + clearLocation.remove(NOISE_AT_END); + if (location != clearLocation) { + result = result.replace(location, clearLocation); + } + } + // + // Для персонажей + // * убираем точки в конце + // + else if (_blockType == TextParagraphType::Character) { + const QString name = /*ScreenplayCharacterParser::name*/ (_blockText); + QString clearName = name.simplified(); + clearName.remove(NOISE_AT_END); + if (name != clearName) { + result = result.replace(name, clearName); + } + } + // + // Ремарка + // * убираем скобки + // + else if (_blockType == TextParagraphType::Parenthetical) { + QString clearParenthetical = _blockText.simplified(); + if (!clearParenthetical.isEmpty() && clearParenthetical.front() == '(') { + clearParenthetical.remove(0, 1); + } + if (!clearParenthetical.isEmpty() && clearParenthetical.back() == ')') { + clearParenthetical.chop(1); + } + result = clearParenthetical; + } + + return result; +} + +QString AbstractQTextDocumentImporter::parseDocument(const ImportOptions& _options, + QTextDocument& _document) const +{ + // + // Найти минимальный отступ слева для всех блоков + // ЗАЧЕМ: во многих программах (Final Draft, Screeviner) сделано так, что поля + // задаются за счёт оступов. Получается что и заглавие сцены и описание действия + // имеют отступы. Так вот это и будет минимальным отступом, который не будем считать + // + int minLeftMargin = 1000; + { + QTextCursor cursor(&_document); + while (!cursor.atEnd()) { + if (minLeftMargin > cursor.blockFormat().leftMargin()) { + minLeftMargin = cursor.blockFormat().leftMargin(); + } + cursor.movePosition(QTextCursor::NextBlock); + cursor.movePosition(QTextCursor::EndOfBlock); + } + } + + QString result; + QXmlStreamWriter writer(&result); + writer.writeStartDocument(); + writer.writeStartElement(xml::kDocumentTag); + writer.writeAttribute(xml::kMimeTypeAttribute, + Domain::mimeTypeFor(Domain::DocumentObjectType::ScreenplayText)); + writer.writeAttribute(xml::kVersionAttribute, "1.0"); + + // + // Для каждого блока текста определяем тип + // + // ... последний стиль блока + auto lastBlockType = TextParagraphType::Undefined; + // ... количество пустых строк + int emptyLines = 0; + bool alreadyInScene = false; + QTextCursor cursor(&_document); + do { + cursor.movePosition(QTextCursor::EndOfBlock); + + // + // Если в блоке есть текст + // + if (!cursor.block().text().simplified().isEmpty()) { + // + // ... определяем тип + // + const auto blockType + = typeForTextCursor(cursor, lastBlockType, emptyLines, minLeftMargin); + + // + // Обработаем блок заголовка сцены в наследниках + // + QString sceneNumber; + if (blockType == TextParagraphType::SceneHeading) { + sceneNumber = processSceneHeading(_options, cursor); + } + + // + // Выполняем корректировки + // + const auto paragraphText + = clearBlockText(blockType, cursor.block().text().simplified()); + + // + // Формируем блок сценария + // + if (blockType == TextParagraphType::SceneHeading) { + if (alreadyInScene) { + writer.writeEndElement(); // контент предыдущей сцены + writer.writeEndElement(); // предыдущая сцена + } + alreadyInScene = true; + + writer.writeStartElement(toString(TextGroupType::Scene)); + writer.writeAttribute(xml::kUuidAttribute, QUuid::createUuid().toString()); + + if (!sceneNumber.isEmpty()) { + writer.writeStartElement(xml::kNumberTag); + writer.writeAttribute(xml::kNumberValueAttribute, sceneNumber); + writer.writeAttribute(xml::kNumberIsCustomAttribute, "true"); + writer.writeAttribute(xml::kNumberIsEatNumberAttribute, "true"); + writer.writeEndElement(); + } + + writer.writeStartElement(xml::kContentTag); + } + writer.writeStartElement(toString(blockType)); + writer.writeStartElement(xml::kValueTag); + writer.writeCDATA(TextHelper::toHtmlEscaped(paragraphText)); + writer.writeEndElement(); // value + // + // Пишем редакторские комментарии + // + writeReviewMarks(writer, cursor); + + // + // Пишем форматирование + // + { + const QTextBlock currentBlock = cursor.block(); + if (!currentBlock.textFormats().isEmpty()) { + writer.writeStartElement(xml::kFormatsTag); + for (const auto& range : currentBlock.textFormats()) { + if (range.format.fontWeight() != QFont::Normal || range.format.fontItalic() + || range.format.fontUnderline()) { + writer.writeEmptyElement(xml::kFormatTag); + writer.writeAttribute(xml::kFromAttribute, + QString::number(range.start)); + writer.writeAttribute(xml::kLengthAttribute, + QString::number(range.length)); + if (range.format.fontWeight() != QFont::Normal) { + writer.writeAttribute(xml::kBoldAttribute, "true"); + } + if (range.format.boolProperty(QTextFormat::FontItalic)) { + writer.writeAttribute(xml::kItalicAttribute, "true"); + } + if (range.format.boolProperty(QTextFormat::TextUnderlineStyle)) { + writer.writeAttribute(xml::kUnderlineAttribute, "true"); + } + if (range.format.boolProperty(QTextFormat::FontStrikeOut)) { + writer.writeAttribute(xml::kStrikethroughAttribute, "true"); + } + } + } + writer.writeEndElement(); + } + } + writer.writeEndElement(); // block type + + // + // Запомним последний стиль блока и обнулим счётчик пустых строк + // + lastBlockType = blockType; + emptyLines = 0; + } + // + // Если в блоке нет текста, то увеличиваем счётчик пустых строк + // + else { + ++emptyLines; + } + + cursor.movePosition(QTextCursor::NextCharacter); + } while (!cursor.atEnd()); + + writer.writeEndDocument(); + + return { result }; +} + +QString AbstractQTextDocumentImporter::processSceneHeading(const ImportOptions& _options, + QTextCursor& _cursor) const +{ + Q_UNUSED(_options) + Q_UNUSED(_cursor) + + return QString(); +} + +TextParagraphType AbstractQTextDocumentImporter::typeForTextCursor(const QTextCursor& _cursor, + TextParagraphType _lastBlockType, + int _prevEmptyLines, + int _minLeftMargin) const +{ + // + // Определим текст блока + // + const QString blockText = _cursor.block().text(); + const QString blockTextUppercase = TextHelper::smartToUpper(blockText); + const QString BlockTextWithoutParentheses + = _cursor.block().text().remove(kTextInParenthesisChecker); + + // + // Для всех нераспознаных блоков ставим тип "Описание действия" + // + TextParagraphType blockType = TextParagraphType::Action; + + // + // Определим некоторые характеристики исследуемого текста + // + // ... стили блока + const QTextBlockFormat blockFormat = _cursor.blockFormat(); + const QTextCharFormat charFormat = _cursor.charFormat(); + // ... текст в верхнем регистре (FIXME: такие строки, как "Я.") + bool textIsUppercase = charFormat.fontCapitalization() == QFont::AllUppercase + || blockText == TextHelper::smartToUpper(blockText); + // ... блоки находящиеся в центре + bool isCentered = !blockFormat.alignment().testFlag(Qt::AlignRight) + && (((blockFormat.leftMargin() + blockFormat.indent()) > 0 + && (blockFormat.leftMargin() + blockFormat.indent()) + > kLeftMarginDelta + _minLeftMargin) + || (blockFormat.alignment().testFlag(Qt::AlignHCenter)) + || blockText.startsWith(kOldSchoolCenteringPrefix)); + + // + // Собственно определение типа + // + { + // + // Самым первым пробуем определить заголовок сцены + // 1. содержит ключевые сокращения места действия + // + if (blockTextUppercase.contains(kPlaceContainsChecker)) { + blockType = TextParagraphType::SceneHeading; + } + + // + // Блоки текста посередине + // + if (isCentered) { + // + // Переход + // 1. в верхнем регистре + // 2. заканчивается двоеточием (без учета пробелов) + // + if (textIsUppercase && blockText.simplified().endsWith(":")) { + blockType = TextParagraphType::Transition; + } + // + // Ремарка + // 1. начинается скобкой + // + else if (blockText.startsWith("(")) { + blockType = TextParagraphType::Parenthetical; + } + // + // Персонаж + // 1. В верхнем регистре + // + else if ((textIsUppercase + || BlockTextWithoutParentheses + == TextHelper::smartToUpper(BlockTextWithoutParentheses)) + && _lastBlockType != TextParagraphType::Character) { + blockType = TextParagraphType::Character; + } + // + // Реплика + // 1. не имеет сверху отступа + // + else if (blockFormat.topMargin() == 0) { + blockType = TextParagraphType::Dialogue; + } + // + // Заметка по тексту + // 1. всё что осталось + // + else { + blockType = TextParagraphType::InlineNote; + } + + } + // + // Не посередине + // + else { + // + // Блоки текста в верхнем регистре + // + if (textIsUppercase) { + // + // Участники сцены + // 1. в верхнем регистре + // 2. идут сразу же после сцены или участника сцены + // 3. не имеют сверху отступа + // + if ((_lastBlockType == TextParagraphType::SceneHeading + || _lastBlockType == TextParagraphType::SceneCharacters) + && _prevEmptyLines == 0 && blockFormat.topMargin() == 0) { + blockType = TextParagraphType::SceneCharacters; + } + // + // Заголовок сцены + // 1. в верхнем регистре + // 2. не имеет отступов + // 3. выровнен по левому краю + // + else if (blockFormat.alignment().testFlag(Qt::AlignLeft) && !isCentered) { + blockType = TextParagraphType::SceneHeading; + } + // + // Переход + // 1. в верхнем регистре + // 2. выровнен по правому краю + // + else if (blockFormat.alignment().testFlag(Qt::AlignRight)) { + blockType = TextParagraphType::Transition; + } + } + } + + // + // Отдельные проверки для блоков, которые могут быть в разных регистрах и в разных местах + // страницы + // + // Титр (формируем, как описание действия) + // 1. начинается со слова ТИТР: + // + if (blockTextUppercase.contains(kTitleChecker)) { + blockType = TextParagraphType::Action; + } + } + + return blockType; +} + +void AbstractQTextDocumentImporter::writeReviewMarks(QXmlStreamWriter& _writer, + QTextCursor& _cursor) const +{ + Q_UNUSED(_writer) + Q_UNUSED(_cursor) +} + +} // namespace BusinessLayer diff --git a/src/corelib/business_layer/import/abstract_qtextdocument_importer.h b/src/corelib/business_layer/import/abstract_qtextdocument_importer.h new file mode 100644 index 000000000..bcc98490d --- /dev/null +++ b/src/corelib/business_layer/import/abstract_qtextdocument_importer.h @@ -0,0 +1,54 @@ +#pragma once + +#include + +class QTextDocument; +class QTextCursor; +class QString; +class QXmlStreamWriter; + +namespace BusinessLayer { + +struct ImportOptions; +enum class TextParagraphType; + +/** + * @brief Класс для импорта из QTextDocument + */ +class CORE_LIBRARY_EXPORT AbstractQTextDocumentImporter +{ +public: + AbstractQTextDocumentImporter(); + virtual ~AbstractQTextDocumentImporter(); + + /** + * @brief Очистка блоков от мусора и их корректировки + */ + QString clearBlockText(TextParagraphType _blockType, const QString& _blockText) const; + + /** + * @brief Получить из QTextDocument xml-строку + */ + QString parseDocument(const ImportOptions& _options, QTextDocument& _document) const; + + /** + * @brief Обработать блок заголовка сцены + */ + virtual QString processSceneHeading(const ImportOptions& _options, QTextCursor& _cursor) const; + + /** + * @brief Определить тип блока в текущей позиции курсора + * с указанием предыдущего типа и количества предшествующих пустых строк + */ + TextParagraphType typeForTextCursor(const QTextCursor& _cursor, + TextParagraphType _lastBlockType, int _prevEmptyLines, + int _minLeftMargin) const; + + /** + * @brief Записать редакторские заметки + */ + virtual void writeReviewMarks(QXmlStreamWriter& _writer, QTextCursor& _cursor) const; +}; + + +} // namespace BusinessLayer diff --git a/src/corelib/business_layer/import/screenplay/screenplay_document_importer.cpp b/src/corelib/business_layer/import/screenplay/screenplay_document_importer.cpp index 179a1cf3d..5a16464c0 100644 --- a/src/corelib/business_layer/import/screenplay/screenplay_document_importer.cpp +++ b/src/corelib/business_layer/import/screenplay/screenplay_document_importer.cpp @@ -7,16 +7,13 @@ #include #include #include -#include #include -#include #include #include #include #include #include -#include #include #include @@ -28,228 +25,12 @@ namespace BusinessLayer { namespace { -/** - * @brief Регулярное выражение для определения блока "Время и место" по наличию слов места - */ -const QRegularExpression kPlaceContainsChecker( - "(INT|EXT|INT/EXT|ИНТ|НАТ|ИНТ/НАТ|ПАВ|ЭКСТ|ИНТ/ЭКСТ)([.]|[ - ])"); -/** - * @brief Регулярное выражение для определения блока "Титр" по наличию ключевых слов - */ -const QRegularExpression kTitleChecker("(^|[^\\S])(TITLE|ТИТР)([:] )"); - /** * @brief Регулярное выражение для определения блока "Время и место" по началу с номера */ const QRegularExpression kStartFromNumberChecker( "^([\\d]{1,}[\\d\\S]{0,})([.]|[-])(([\\d\\S]{1,})([.]|)|) "); -/** - * @brief Допущение для блоков, которые по идее вообще не должны иметь отступа в пикселях (16 мм) - */ -const int kLeftMarginDelta = 60; - -/** - * @brief Некоторые программы выравнивают текст при помощи пробелов - */ -const QString kOldSchoolCenteringPrefix = " "; - -/** - * @brief Определить тип блока в текущей позиции курсора - * с указанием предыдущего типа и количества предшествующих пустых строк - */ -TextParagraphType typeForTextCursor(const QTextCursor& _cursor, TextParagraphType _lastBlockType, - int _prevEmptyLines, int _minLeftMargin) -{ - // - // Определим текст блока - // - const QString blockText = _cursor.block().text(); - const QString blockTextUppercase = TextHelper::smartToUpper(blockText); - - // - // Для всех нераспознаных блоков ставим тип "Описание действия" - // - TextParagraphType blockType = TextParagraphType::Action; - - // - // Определим некоторые характеристики исследуемого текста - // - // ... стили блока - const QTextBlockFormat blockFormat = _cursor.blockFormat(); - const QTextCharFormat charFormat = _cursor.charFormat(); - // ... текст в верхнем регистре (FIXME: такие строки, как "Я.") - bool textIsUppercase = charFormat.fontCapitalization() == QFont::AllUppercase - || blockText == TextHelper::smartToUpper(blockText); - // ... блоки находящиеся в центре - bool isCentered = !blockFormat.alignment().testFlag(Qt::AlignRight) - && (((blockFormat.leftMargin() + blockFormat.indent()) > 0 - && (blockFormat.leftMargin() + blockFormat.indent()) - > kLeftMarginDelta + _minLeftMargin) - || (blockFormat.alignment().testFlag(Qt::AlignHCenter)) - || blockText.startsWith(kOldSchoolCenteringPrefix)); - - // - // Собственно определение типа - // - { - // - // Самым первым пробуем определить время и место - // 1. содержит ключевые сокращения места действия или начинается с номера сцены - // - if (blockTextUppercase.contains(kPlaceContainsChecker) - || blockTextUppercase.contains(kStartFromNumberChecker)) { - blockType = TextParagraphType::SceneHeading; - } - // - // Блоки текста посередине - // - else if (isCentered) { - // - // Персонаж - // 1. В верхнем регистре - // - if (textIsUppercase && _lastBlockType != TextParagraphType::Character) { - blockType = TextParagraphType::Character; - } - // - // Ремарка - // 1. начинается со скобки - // - else if (blockText.startsWith("(")) { - blockType = TextParagraphType::Parenthetical; - } - // - // Реплика - // 1. всё что осталось - // - else { - blockType = TextParagraphType::Dialogue; - } - - } - // - // Не посередине - // - else { - // - // Блоки текста в верхнем регистре - // - if (textIsUppercase) { - // - // Участника сцены - // 1. в верхнем регистре - // 2. идут сразу же после времени и места - // 3. не имеют сверху отступа - // - if (_lastBlockType == TextParagraphType::SceneHeading && _prevEmptyLines == 0 - && blockFormat.topMargin() == 0) { - blockType = TextParagraphType::SceneCharacters; - } - // - // Примечание - // 1. всё что осталось и не имеет отступов - // 2. выровнено по левому краю - // - else if (blockFormat.alignment().testFlag(Qt::AlignLeft) && !isCentered) { - blockType = TextParagraphType::Action; - } - // - // Переход - // 1. всё что осталось и выровнено по правому краю - // - else if (blockFormat.alignment().testFlag(Qt::AlignRight)) { - blockType = TextParagraphType::Transition; - } - } - } - - // - // Отдельные проверки для блоков, которые могут быть в разных регистрах и в разных местах - // страницы - // - // Титр (формируем, как описание действия) - // 1. начинается со слова ТИТР: - // - if (blockTextUppercase.contains(kTitleChecker)) { - blockType = TextParagraphType::Action; - } - } - - return blockType; -} - -/** - * @brief Шум, который может встречаться в тексте - */ -const QString NOISE("([.]|[,]|[:]|[ ]|[-]){1,}"); - -/** - * @brief Регулярное выражение для удаления мусора в начале текста - */ -const QRegularExpression NOISE_AT_START("^" + NOISE); - -/** - * @brief Регулярное выражение для удаления мусора в конце текста - */ -const QRegularExpression NOISE_AT_END(NOISE + "$"); - -/** - * @brief Очистка блоков от мусора и их корректировки - */ -static QString clearBlockText(TextParagraphType _blockType, const QString& _blockText) -{ - QString result = _blockText; - - // - // Удаляем длинные тире - // - result = result.replace("–", "-"); - - // - // Для блока времени и места: - // * всевозможные "инт - " меняем на "инт. " - // * убираем точки в конце названия локации - // - if (_blockType == TextParagraphType::SceneHeading) { - const QString location = ScreenplaySceneHeadingParser::location(_blockText); - QString clearLocation = location.simplified(); - clearLocation.remove(NOISE_AT_START); - clearLocation.remove(NOISE_AT_END); - if (location != clearLocation) { - result = result.replace(location, clearLocation); - } - } - // - // Для персонажей - // * убираем точки в конце - // - else if (_blockType == TextParagraphType::Character) { - const QString name = ScreenplayCharacterParser::name(_blockText); - QString clearName = name.simplified(); - clearName.remove(NOISE_AT_END); - if (name != clearName) { - result = result.replace(name, clearName); - } - } - // - // Ремарка - // * убираем скобки - // - else if (_blockType == TextParagraphType::Parenthetical) { - QString clearParenthetical = _blockText.simplified(); - if (!clearParenthetical.isEmpty() && clearParenthetical.front() == '(') { - clearParenthetical.remove(0, 1); - } - if (!clearParenthetical.isEmpty() && clearParenthetical.back() == ')') { - clearParenthetical.chop(1); - } - result = clearParenthetical; - } - - return result; -} - } // namespace AbstractScreenplayImporter::Documents ScreenplayDocumentImporter::importDocuments( @@ -395,19 +176,19 @@ AbstractScreenplayImporter::Documents ScreenplayDocumentImporter::importDocument QVector ScreenplayDocumentImporter::importScreenplays( const ScreenplayImportOptions& _options) const { + Screenplay screenplay; + screenplay.name = QFileInfo(_options.filePath).completeBaseName(); + if (_options.importText == false) { - return {}; + return { screenplay }; } - Screenplay result; - result.name = QFileInfo(_options.filePath).completeBaseName(); - // // Открываем файл // QFile documentFile(_options.filePath); if (!documentFile.open(QIODevice::ReadOnly)) { - return { result }; + return { screenplay }; } // @@ -419,229 +200,86 @@ QVector ScreenplayDocumentImporter::impo reader->read(&documentFile, &documentForImport); } - // - // Найти минимальный отступ слева для всех блоков - // ЗАЧЕМ: во многих программах (Final Draft, Screeviner) сделано так, что поля - // задаются за счёт оступов. Получается что и заглавие сцены и описание действия - // имеют отступы. Так вот это и будет минимальным отступом, который не будем считать - // - int minLeftMargin = 1000; - { - QTextCursor cursor(&documentForImport); - while (!cursor.atEnd()) { - if (minLeftMargin > cursor.blockFormat().leftMargin()) { - minLeftMargin = cursor.blockFormat().leftMargin(); - } - - cursor.movePosition(QTextCursor::NextBlock); - cursor.movePosition(QTextCursor::EndOfBlock); - } - } - - // - // Преобразовать его в xml-строку - // - QTextCursor cursor(&documentForImport); - - QXmlStreamWriter writer(&result.text); - writer.writeStartDocument(); - writer.writeStartElement(xml::kDocumentTag); - writer.writeAttribute(xml::kMimeTypeAttribute, - Domain::mimeTypeFor(Domain::DocumentObjectType::ScreenplayText)); - writer.writeAttribute(xml::kVersionAttribute, "1.0"); + screenplay.text = parseDocument(_options, documentForImport); - // - // Для каждого блока текста определяем тип - // - // ... последний стиль блока - auto lastBlockType = TextParagraphType::Undefined; - // ... количество пустых строк - int emptyLines = 0; - bool alreadyInScene = false; - do { - cursor.movePosition(QTextCursor::EndOfBlock); + return { screenplay }; +} - // - // Если в блоке есть текст - // - if (!cursor.block().text().simplified().isEmpty()) { - // - // ... определяем тип - // - const auto blockType - = typeForTextCursor(cursor, lastBlockType, emptyLines, minLeftMargin); - // - // Если текущий тип "Время и место", то удалим номер сцены - // - QString sceneNumber; - if (blockType == TextParagraphType::SceneHeading) { - const auto match - = kStartFromNumberChecker.match(cursor.block().text().simplified()); - if (match.hasMatch()) { - cursor.movePosition(QTextCursor::StartOfBlock); - cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor, - match.capturedEnd()); - if (cursor.hasSelection()) { - if (_options.keepSceneNumbers) { - sceneNumber = cursor.selectedText().trimmed(); - if (sceneNumber.endsWith('.')) { - sceneNumber.chop(1); - } - } - cursor.deleteChar(); - } - cursor.movePosition(QTextCursor::EndOfBlock); +QString ScreenplayDocumentImporter::processSceneHeading(const ImportOptions& _options, + QTextCursor& _cursor) const +{ + QString sceneNumber; + const auto match = kStartFromNumberChecker.match(_cursor.block().text().simplified()); + if (match.hasMatch()) { + _cursor.movePosition(QTextCursor::StartOfBlock); + _cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor, + match.capturedEnd()); + if (_cursor.hasSelection()) { + const auto& options = static_cast(_options); + if (options.keepSceneNumbers) { + sceneNumber = _cursor.selectedText().trimmed(); + if (sceneNumber.endsWith('.')) { + sceneNumber.chop(1); } } + _cursor.deleteChar(); + } + _cursor.movePosition(QTextCursor::EndOfBlock); + } + return sceneNumber; +} - // - // Выполняем корректировки - // - const auto paragraphText - = clearBlockText(blockType, cursor.block().text().simplified()); - - // - // Формируем блок сценария - // - if (blockType == TextParagraphType::SceneHeading) { - if (alreadyInScene) { - writer.writeEndElement(); // контент предыдущей сцены - writer.writeEndElement(); // предыдущая сцена +void ScreenplayDocumentImporter::writeReviewMarks(QXmlStreamWriter& _writer, + QTextCursor& _cursor) const +{ + const QTextBlock currentBlock = _cursor.block(); + if (!currentBlock.textFormats().isEmpty()) { + _writer.writeStartElement(xml::kReviewMarksTag); + for (const auto& range : currentBlock.textFormats()) { + if (range.format.boolProperty(Docx::IsForeground) + || range.format.boolProperty(Docx::IsBackground) + || range.format.boolProperty(Docx::IsHighlight) + || range.format.boolProperty(Docx::IsComment)) { + _writer.writeStartElement(xml::kReviewMarkTag); + _writer.writeAttribute(xml::kFromAttribute, QString::number(range.start)); + _writer.writeAttribute(xml::kLengthAttribute, QString::number(range.length)); + if (range.format.hasProperty(QTextFormat::ForegroundBrush)) { + _writer.writeAttribute(xml::kColorAttribute, + range.format.foreground().color().name()); } - alreadyInScene = true; - - writer.writeStartElement(toString(TextGroupType::Scene)); - writer.writeAttribute(xml::kUuidAttribute, QUuid::createUuid().toString()); - - if (!sceneNumber.isEmpty()) { - writer.writeStartElement(xml::kNumberTag); - writer.writeAttribute(xml::kNumberValueAttribute, sceneNumber); - writer.writeAttribute(xml::kNumberIsCustomAttribute, "true"); - writer.writeAttribute(xml::kNumberIsEatNumberAttribute, "true"); - writer.writeEndElement(); + if (range.format.hasProperty(QTextFormat::BackgroundBrush)) { + _writer.writeAttribute(xml::kBackgroundColorAttribute, + range.format.background().color().name()); } - - writer.writeStartElement(xml::kContentTag); - } - writer.writeStartElement(toString(blockType)); - writer.writeStartElement(xml::kValueTag); - writer.writeCDATA(TextHelper::toHtmlEscaped(paragraphText)); - writer.writeEndElement(); // value - // - // Пишем редакторские комментарии - // - { - const QTextBlock currentBlock = cursor.block(); - if (!currentBlock.textFormats().isEmpty()) { - writer.writeStartElement(xml::kReviewMarksTag); - for (const auto& range : currentBlock.textFormats()) { - if (range.format.boolProperty(Docx::IsForeground) - || range.format.boolProperty(Docx::IsBackground) - || range.format.boolProperty(Docx::IsHighlight) - || range.format.boolProperty(Docx::IsComment)) { - writer.writeStartElement(xml::kReviewMarkTag); - writer.writeAttribute(xml::kFromAttribute, - QString::number(range.start)); - writer.writeAttribute(xml::kLengthAttribute, - QString::number(range.length)); - if (range.format.hasProperty(QTextFormat::ForegroundBrush)) { - writer.writeAttribute(xml::kColorAttribute, - range.format.foreground().color().name()); - } - if (range.format.hasProperty(QTextFormat::BackgroundBrush)) { - writer.writeAttribute(xml::kBackgroundColorAttribute, - range.format.background().color().name()); - } - // - // ... комментарии - // - QStringList authors - = range.format.property(Docx::CommentsAuthors).toStringList(); - if (authors.isEmpty()) { - authors.append(DataStorageLayer::StorageFacade::settingsStorage() - ->accountName()); - } - QStringList dates - = range.format.property(Docx::CommentsDates).toStringList(); - if (dates.isEmpty()) { - dates.append(QDateTime::currentDateTime().toString(Qt::ISODate)); - } - QStringList comments - = range.format.property(Docx::Comments).toStringList(); - if (comments.isEmpty()) { - comments.append(QString()); - } - for (int commentIndex = 0; commentIndex < comments.size(); - ++commentIndex) { - writer.writeStartElement(xml::kCommentTag); - writer.writeAttribute(xml::kAuthorAttribute, - authors.at(commentIndex)); - writer.writeAttribute(xml::kDateAttribute, dates.at(commentIndex)); - writer.writeCDATA( - TextHelper::toHtmlEscaped(comments.at(commentIndex))); - writer.writeEndElement(); // comment - } - // - writer.writeEndElement(); // review mark - } - } - writer.writeEndElement(); // review marks + // + // ... комментарии + // + QStringList authors = range.format.property(Docx::CommentsAuthors).toStringList(); + if (authors.isEmpty()) { + authors.append( + DataStorageLayer::StorageFacade::settingsStorage()->accountName()); } - } - - // - // Пишем форматирование - // - { - const QTextBlock currentBlock = cursor.block(); - if (!currentBlock.textFormats().isEmpty()) { - writer.writeStartElement(xml::kFormatsTag); - for (const auto& range : currentBlock.textFormats()) { - if (range.format.fontWeight() != QFont::Normal || range.format.fontItalic() - || range.format.fontUnderline()) { - writer.writeEmptyElement(xml::kFormatTag); - writer.writeAttribute(xml::kFromAttribute, - QString::number(range.start)); - writer.writeAttribute(xml::kLengthAttribute, - QString::number(range.length)); - if (range.format.fontWeight() != QFont::Normal) { - writer.writeAttribute(xml::kBoldAttribute, "true"); - } - if (range.format.boolProperty(QTextFormat::FontItalic)) { - writer.writeAttribute(xml::kItalicAttribute, "true"); - } - if (range.format.boolProperty(QTextFormat::TextUnderlineStyle)) { - writer.writeAttribute(xml::kUnderlineAttribute, "true"); - } - if (range.format.boolProperty(QTextFormat::FontStrikeOut)) { - writer.writeAttribute(xml::kStrikethroughAttribute, "true"); - } - } - } - writer.writeEndElement(); + QStringList dates = range.format.property(Docx::CommentsDates).toStringList(); + if (dates.isEmpty()) { + dates.append(QDateTime::currentDateTime().toString(Qt::ISODate)); } + QStringList comments = range.format.property(Docx::Comments).toStringList(); + if (comments.isEmpty()) { + comments.append(QString()); + } + for (int commentIndex = 0; commentIndex < comments.size(); ++commentIndex) { + _writer.writeStartElement(xml::kCommentTag); + _writer.writeAttribute(xml::kAuthorAttribute, authors.at(commentIndex)); + _writer.writeAttribute(xml::kDateAttribute, dates.at(commentIndex)); + _writer.writeCDATA(TextHelper::toHtmlEscaped(comments.at(commentIndex))); + _writer.writeEndElement(); // comment + } + // + _writer.writeEndElement(); // review mark } - writer.writeEndElement(); // block type - - // - // Запомним последний стиль блока и обнулим счётчик пустых строк - // - lastBlockType = blockType; - emptyLines = 0; } - // - // Если в блоке нет текста, то увеличиваем счётчик пустых строк - // - else { - ++emptyLines; - } - - cursor.movePosition(QTextCursor::NextCharacter); - } while (!cursor.atEnd()); - - writer.writeEndDocument(); - - return { result }; + _writer.writeEndElement(); // review marks + } } } // namespace BusinessLayer diff --git a/src/corelib/business_layer/import/screenplay/screenplay_document_importer.h b/src/corelib/business_layer/import/screenplay/screenplay_document_importer.h index 39525327a..d83d0b7f7 100644 --- a/src/corelib/business_layer/import/screenplay/screenplay_document_importer.h +++ b/src/corelib/business_layer/import/screenplay/screenplay_document_importer.h @@ -1,14 +1,17 @@ #pragma once #include "abstract_screenplay_importer.h" +#include "business_layer/import/abstract_qtextdocument_importer.h" +class QTextCursor; namespace BusinessLayer { /** - * @brief Импортер сценария из файлов Trelby + * @brief Импортер сценария из файлов Docx */ -class CORE_LIBRARY_EXPORT ScreenplayDocumentImporter : public AbstractScreenplayImporter +class CORE_LIBRARY_EXPORT ScreenplayDocumentImporter : public AbstractScreenplayImporter, + public AbstractQTextDocumentImporter { public: ScreenplayDocumentImporter() = default; @@ -22,6 +25,16 @@ class CORE_LIBRARY_EXPORT ScreenplayDocumentImporter : public AbstractScreenplay * @brief Сформировать xml-сценария во внутреннем формате */ QVector importScreenplays(const ScreenplayImportOptions& _options) const override; + + /** + * @brief Обработать блок сцены + */ + QString processSceneHeading(const ImportOptions& _options, QTextCursor& _cursor) const override; + + /** + * @brief Записать редакторские заметки + */ + void writeReviewMarks(QXmlStreamWriter& _writer, QTextCursor& _cursor) const override; }; } // namespace BusinessLayer diff --git a/src/corelib/business_layer/import/screenplay/screenplay_pdf_importer.cpp b/src/corelib/business_layer/import/screenplay/screenplay_pdf_importer.cpp index 58eb781bf..aaf2a8046 100644 --- a/src/corelib/business_layer/import/screenplay/screenplay_pdf_importer.cpp +++ b/src/corelib/business_layer/import/screenplay/screenplay_pdf_importer.cpp @@ -3,522 +3,36 @@ #include "TableExtraction.h" #include "screenplay_import_options.h" -#include -#include -#include -#include -#include -#include - -#include -#include #include -#include -#include -#include #include -#include - -#include namespace BusinessLayer { -namespace { - -/** - * @brief Регулярное выражение для определения блока "Время и место" по наличию слов места - */ -const QRegularExpression kPlaceContainsChecker( - "^(INT|EXT|INT/EXT|ИНТ|НАТ|ИНТ/НАТ|ПАВ|ЭКСТ|ИНТ/ЭКСТ)([.]|[ - ])"); - -/** - * @brief Регулярное выражение для определения блока "Титр" по наличию ключевых слов - */ -const QRegularExpression kTitleChecker("(^|[^\\S])(TITLE|ТИТР)([:] )"); - -/** - * @brief Регулярное выражение для определения блока "Время и место" по началу с номера - */ -const QRegularExpression kStartFromNumberChecker( - "^([\\d]{1,}[\\d\\S]{0,})([.]|[-])(([\\d\\S]{1,})([.]|)|) "); - -/** - * @brief Регулярное выражение для определения текста в скобках - */ -const QRegularExpression kTextInParenthesisChecker(".{1,}\\([^\\)]{1,}\\)"); - -/** - * @brief Допущение для блоков, которые по идее вообще не должны иметь отступа в пикселях (16 мм) - */ -const int kLeftMarginDelta = 60; - -/** - * @brief Некоторые программы выравнивают текст при помощи пробелов - */ -const QString kOldSchoolCenteringPrefix = " "; - -/** - * @brief Шум, который может встречаться в тексте - */ -const QString NOISE("([.]|[,]|[:]|[ ]|[-]){1,}"); - -/** - * @brief Регулярное выражение для удаления мусора в начале текста - */ -const QRegularExpression NOISE_AT_START("^" + NOISE); - -/** - * @brief Регулярное выражение для удаления мусора в конце текста - */ -const QRegularExpression NOISE_AT_END(NOISE + "$"); - -/** - * @brief Очистка блоков от мусора и их корректировки - */ -static QString clearBlockText(TextParagraphType _blockType, const QString& _blockText) -{ - QString result = _blockText; - - // - // Удаляем длинные тире - // - result = result.replace("–", "-"); - - // - // Для блока времени и места: - // * всевозможные "инт - " меняем на "инт. " - // * убираем точки в конце названия локации - // - if (_blockType == TextParagraphType::SceneHeading) { - const QString location = ScreenplaySceneHeadingParser::location(_blockText); - QString clearLocation = location.simplified(); - clearLocation.remove(NOISE_AT_START); - clearLocation.remove(NOISE_AT_END); - if (location != clearLocation) { - result = result.replace(location, clearLocation); - } - } - // - // Для персонажей - // * убираем точки в конце - // - else if (_blockType == TextParagraphType::Character) { - const QString name = ScreenplayCharacterParser::name(_blockText); - QString clearName = name.simplified(); - clearName.remove(NOISE_AT_END); - if (name != clearName) { - result = result.replace(name, clearName); - } - } - // - // Ремарка - // * убираем скобки - // - else if (_blockType == TextParagraphType::Parenthetical) { - QString clearParenthetical = _blockText.simplified(); - if (!clearParenthetical.isEmpty() && clearParenthetical.front() == '(') { - clearParenthetical.remove(0, 1); - } - if (!clearParenthetical.isEmpty() && clearParenthetical.back() == ')') { - clearParenthetical.chop(1); - } - result = clearParenthetical; - } - - return result; -} - -/** - * @brief Определить тип блока в текущей позиции курсора - * с указанием предыдущего типа и количества предшествующих пустых строк - */ -TextParagraphType typeForTextCursor(const QTextCursor& _cursor, TextParagraphType _lastBlockType, - int _prevEmptyLines, int _minLeftMargin) +ScreenplayPdfImporter::ScreenplayPdfImporter() + : AbstractScreenplayImporter() + , AbstractQTextDocumentImporter() { - // - // Определим текст блока - // - const QString blockText = _cursor.block().text(); - const QString blockTextUppercase = TextHelper::smartToUpper(blockText); - const QString BlockTextWithoutParentheses - = _cursor.block().text().remove(kTextInParenthesisChecker); - - // - // Для всех нераспознаных блоков ставим тип "Описание действия" - // - TextParagraphType blockType = TextParagraphType::Action; - - // - // Определим некоторые характеристики исследуемого текста - // - // ... стили блока - const QTextBlockFormat blockFormat = _cursor.blockFormat(); - const QTextCharFormat charFormat = _cursor.charFormat(); - // ... текст в верхнем регистре (FIXME: такие строки, как "Я.") - bool textIsUppercase = charFormat.fontCapitalization() == QFont::AllUppercase - || blockText == TextHelper::smartToUpper(blockText); - // ... блоки находящиеся в центре - bool isCentered = !blockFormat.alignment().testFlag(Qt::AlignRight) - && (((blockFormat.leftMargin() + blockFormat.indent()) > 0 - && (blockFormat.leftMargin() + blockFormat.indent()) - > kLeftMarginDelta + _minLeftMargin) - || (blockFormat.alignment().testFlag(Qt::AlignHCenter)) - || blockText.startsWith(kOldSchoolCenteringPrefix)); - - // - // Собственно определение типа - // - { - // - // Блоки текста посередине - // - if (isCentered) { - // - // Переход - // 1. в верхнем регистре - // 2. заканчивается двоеточием (без учета пробелов) - // - if (textIsUppercase && blockText.simplified().endsWith(":")) { - blockType = TextParagraphType::Transition; - } - // - // Ремарка - // 1. начинается скобкой - // - else if (blockText.startsWith("(")) { - blockType = TextParagraphType::Parenthetical; - } - // - // Персонаж - // 1. В верхнем регистре - // - else if ((textIsUppercase - || BlockTextWithoutParentheses - == TextHelper::smartToUpper(BlockTextWithoutParentheses)) - && _lastBlockType != TextParagraphType::Character) { - blockType = TextParagraphType::Character; - } - // - // Реплика - // 1. не имеет сверху отступа - // - else if (blockFormat.topMargin() == 0) { - blockType = TextParagraphType::Dialogue; - } - // - // Заметка по тексту - // 1. всё что осталось - // - else { - blockType = TextParagraphType::InlineNote; - } - - } - // - // Не посередине - // - else { - // - // Блоки текста в верхнем регистре - // - if (textIsUppercase) { - // - // Участника сцены - // 1. в верхнем регистре - // 2. идут сразу же после сцены или участника сцены - // 3. не имеют сверху отступа - // - if ((_lastBlockType == TextParagraphType::SceneHeading - || _lastBlockType == TextParagraphType::SceneCharacters) - && _prevEmptyLines == 0 && blockFormat.topMargin() == 0) { - blockType = TextParagraphType::SceneCharacters; - } - // - // Заголовок сцены - // 1. в верхнем регистре - // 2. не имеет отступов - // 3. выровнен по левому краю - // - else if (blockFormat.alignment().testFlag(Qt::AlignLeft) && !isCentered) { - blockType = TextParagraphType::SceneHeading; - } - // - // Переход - // 1. в верхнем регистре - // 2. выровнен по правому краю - // - else if (blockFormat.alignment().testFlag(Qt::AlignRight)) { - blockType = TextParagraphType::Transition; - } - } - } - - // - // Отдельные проверки для блоков, которые могут быть в разных регистрах и в разных местах - // страницы - // - // Титр (формируем, как описание действия) - // 1. начинается со слова ТИТР: - // - if (blockTextUppercase.contains(kTitleChecker)) { - blockType = TextParagraphType::Action; - } - } - - return blockType; } -/** - * @brief Получить из QTextDocument xml-строку - */ -AbstractScreenplayImporter::Screenplay parseDocument(const ScreenplayImportOptions& _options, - QTextDocument* document) -{ - // - // Найти минимальный отступ слева для всех блоков - // ЗАЧЕМ: во многих программах (Final Draft, Screeviner) сделано так, что поля - // задаются за счёт оступов. Получается что и заглавие сцены и описание действия - // имеют отступы. Так вот это и будет минимальным отступом, который не будем считать - // - int minLeftMargin = 1000; - { - QTextCursor cursor(document); - while (!cursor.atEnd()) { - if (minLeftMargin > cursor.blockFormat().leftMargin()) { - minLeftMargin = cursor.blockFormat().leftMargin(); - } - - cursor.movePosition(QTextCursor::NextBlock); - cursor.movePosition(QTextCursor::EndOfBlock); - } - } - - - AbstractScreenplayImporter::Screenplay result; - result.name = QFileInfo(_options.filePath).completeBaseName(); - - // - // Преобразовать его в xml-строку - // - QTextCursor cursor(document); - - QXmlStreamWriter writer(&result.text); - writer.writeStartDocument(); - writer.writeStartElement(xml::kDocumentTag); - writer.writeAttribute(xml::kMimeTypeAttribute, - Domain::mimeTypeFor(Domain::DocumentObjectType::ScreenplayText)); - writer.writeAttribute(xml::kVersionAttribute, "1.0"); - - // - // Для каждого блока текста определяем тип - // - // ... последний стиль блока - auto lastBlockType = TextParagraphType::Undefined; - // ... количество пустых строк - int emptyLines = 0; - bool alreadyInScene = false; - do { - cursor.movePosition(QTextCursor::EndOfBlock); - - // - // Если в блоке есть текст - // - if (!cursor.block().text().simplified().isEmpty()) { - // - // ... определяем тип - // - const auto blockType - = typeForTextCursor(cursor, lastBlockType, emptyLines, minLeftMargin); - - // - // Если текущий тип "Время и место", то удалим номер сцены - // - QString sceneNumber; - if (blockType == TextParagraphType::SceneHeading) { - const auto match - = kStartFromNumberChecker.match(cursor.block().text().simplified()); - if (match.hasMatch()) { - cursor.movePosition(QTextCursor::StartOfBlock); - cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor, - match.capturedEnd()); - if (cursor.hasSelection()) { - if (_options.keepSceneNumbers) { - sceneNumber = cursor.selectedText().trimmed(); - if (sceneNumber.endsWith('.')) { - sceneNumber.chop(1); - } - } - cursor.deleteChar(); - } - cursor.movePosition(QTextCursor::EndOfBlock); - } - } - - // - // Выполняем корректировки - // - const auto paragraphText - = clearBlockText(blockType, cursor.block().text().simplified()); - - // - // Формируем блок сценария - // - if (blockType == TextParagraphType::SceneHeading) { - if (alreadyInScene) { - writer.writeEndElement(); // контент предыдущей сцены - writer.writeEndElement(); // предыдущая сцена - } - alreadyInScene = true; - - writer.writeStartElement(toString(TextGroupType::Scene)); - writer.writeAttribute(xml::kUuidAttribute, QUuid::createUuid().toString()); - - if (!sceneNumber.isEmpty()) { - writer.writeStartElement(xml::kNumberTag); - writer.writeAttribute(xml::kNumberValueAttribute, sceneNumber); - writer.writeAttribute(xml::kNumberIsCustomAttribute, "true"); - writer.writeAttribute(xml::kNumberIsEatNumberAttribute, "true"); - writer.writeEndElement(); - } - - writer.writeStartElement(xml::kContentTag); - } - writer.writeStartElement(toString(blockType)); - writer.writeStartElement(xml::kValueTag); - writer.writeCDATA(TextHelper::toHtmlEscaped(paragraphText)); - writer.writeEndElement(); // value - // - // Пишем редакторские комментарии - // - { - const QTextBlock currentBlock = cursor.block(); - if (!currentBlock.textFormats().isEmpty()) { - writer.writeStartElement(xml::kReviewMarksTag); - for (const auto& range : currentBlock.textFormats()) { - if (range.format.boolProperty(Docx::IsForeground) - || range.format.boolProperty(Docx::IsBackground) - || range.format.boolProperty(Docx::IsHighlight) - || range.format.boolProperty(Docx::IsComment)) { - writer.writeStartElement(xml::kReviewMarkTag); - writer.writeAttribute(xml::kFromAttribute, - QString::number(range.start)); - writer.writeAttribute(xml::kLengthAttribute, - QString::number(range.length)); - if (range.format.hasProperty(QTextFormat::ForegroundBrush)) { - writer.writeAttribute(xml::kColorAttribute, - range.format.foreground().color().name()); - } - if (range.format.hasProperty(QTextFormat::BackgroundBrush)) { - writer.writeAttribute(xml::kBackgroundColorAttribute, - range.format.background().color().name()); - } - // - // ... комментарии - // - QStringList authors - = range.format.property(Docx::CommentsAuthors).toStringList(); - if (authors.isEmpty()) { - authors.append(DataStorageLayer::StorageFacade::settingsStorage() - ->accountName()); - } - QStringList dates - = range.format.property(Docx::CommentsDates).toStringList(); - if (dates.isEmpty()) { - dates.append(QDateTime::currentDateTime().toString(Qt::ISODate)); - } - QStringList comments - = range.format.property(Docx::Comments).toStringList(); - if (comments.isEmpty()) { - comments.append(QString()); - } - for (int commentIndex = 0; commentIndex < comments.size(); - ++commentIndex) { - writer.writeStartElement(xml::kCommentTag); - writer.writeAttribute(xml::kAuthorAttribute, - authors.at(commentIndex)); - writer.writeAttribute(xml::kDateAttribute, dates.at(commentIndex)); - writer.writeCDATA( - TextHelper::toHtmlEscaped(comments.at(commentIndex))); - writer.writeEndElement(); // comment - } - // - writer.writeEndElement(); // review mark - } - } - writer.writeEndElement(); // review marks - } - } - - // - // Пишем форматирование - // - { - const QTextBlock currentBlock = cursor.block(); - if (!currentBlock.textFormats().isEmpty()) { - writer.writeStartElement(xml::kFormatsTag); - for (const auto& range : currentBlock.textFormats()) { - if (range.format.fontWeight() != QFont::Normal || range.format.fontItalic() - || range.format.fontUnderline()) { - writer.writeEmptyElement(xml::kFormatTag); - writer.writeAttribute(xml::kFromAttribute, - QString::number(range.start)); - writer.writeAttribute(xml::kLengthAttribute, - QString::number(range.length)); - if (range.format.fontWeight() != QFont::Normal) { - writer.writeAttribute(xml::kBoldAttribute, "true"); - } - if (range.format.boolProperty(QTextFormat::FontItalic)) { - writer.writeAttribute(xml::kItalicAttribute, "true"); - } - if (range.format.boolProperty(QTextFormat::TextUnderlineStyle)) { - writer.writeAttribute(xml::kUnderlineAttribute, "true"); - } - if (range.format.boolProperty(QTextFormat::FontStrikeOut)) { - writer.writeAttribute(xml::kStrikethroughAttribute, "true"); - } - } - } - writer.writeEndElement(); - } - } - writer.writeEndElement(); // block type - - // - // Запомним последний стиль блока и обнулим счётчик пустых строк - // - lastBlockType = blockType; - emptyLines = 0; - } - // - // Если в блоке нет текста, то увеличиваем счётчик пустых строк - // - else { - ++emptyLines; - } - - cursor.movePosition(QTextCursor::NextCharacter); - } while (!cursor.atEnd()); - - writer.writeEndDocument(); - - return { result }; -} +ScreenplayPdfImporter::~ScreenplayPdfImporter() = default; -} // namespace -ScreenplayPdfImporter::ScreenplayPdfImporter() - : ScreenplayFountainImporter() +AbstractImporter::Documents ScreenplayPdfImporter::importDocuments( + const ImportOptions& _options) const { + Documents documents; + return documents; } -ScreenplayPdfImporter::~ScreenplayPdfImporter() = default; - QVector ScreenplayPdfImporter::importScreenplays( const ScreenplayImportOptions& _options) const { + Screenplay screenplay; + screenplay.name = QFileInfo(_options.filePath).completeBaseName(); + if (_options.importText == false) { - return {}; + return { screenplay }; } // @@ -530,14 +44,18 @@ QVector ScreenplayPdfImporter::importScr } // - // Извлекаем текст в QTextDocument + // Преобразовать заданный документ в QTextDocument + // Используем TableExtraction, чтобы извлечь не только текст, но и линии // TableExtraction tableExtractor; tableExtractor.ExtractTables(_options.filePath.toStdString(), 0, -1, true); QTextDocument document; tableExtractor.GetResultsAsDocument(document); - Screenplay screenplay = parseDocument(_options, &document); + // + // Парсим QTextDocument + // + screenplay.text = parseDocument(_options, document); return { screenplay }; } diff --git a/src/corelib/business_layer/import/screenplay/screenplay_pdf_importer.h b/src/corelib/business_layer/import/screenplay/screenplay_pdf_importer.h index 2247df42a..2d8402759 100644 --- a/src/corelib/business_layer/import/screenplay/screenplay_pdf_importer.h +++ b/src/corelib/business_layer/import/screenplay/screenplay_pdf_importer.h @@ -1,16 +1,25 @@ #pragma once -#include "screenplay_fountain_importer.h" - +#include "abstract_screenplay_importer.h" +#include "business_layer/import/abstract_qtextdocument_importer.h" namespace BusinessLayer { -class CORE_LIBRARY_EXPORT ScreenplayPdfImporter : public ScreenplayFountainImporter +/** + * @brief Импортер сценария из файлов Pdf + */ +class CORE_LIBRARY_EXPORT ScreenplayPdfImporter : public AbstractScreenplayImporter, + public AbstractQTextDocumentImporter { public: ScreenplayPdfImporter(); ~ScreenplayPdfImporter() override; + /** + * @brief Импорт докуметов (всех, кроме сценариев) + */ + Documents importDocuments(const ImportOptions& _options) const override; + /** * @brief Импортировать сценарии */ diff --git a/src/corelib/corelib.pro b/src/corelib/corelib.pro index f506b6079..e7728ea49 100644 --- a/src/corelib/corelib.pro +++ b/src/corelib/corelib.pro @@ -217,6 +217,7 @@ SOURCES += \ business_layer/export/stageplay/stageplay_pdf_exporter.cpp \ business_layer/import/abstract_fountain_importer.cpp \ business_layer/import/abstract_markdown_importer.cpp \ + business_layer/import/abstract_qtextdocument_importer.cpp \ business_layer/import/audioplay/audioplay_fountain_importer.cpp \ business_layer/import/comic_book/comic_book_fountain_importer.cpp \ business_layer/import/novel/novel_markdown_importer.cpp \ @@ -565,6 +566,7 @@ HEADERS += \ business_layer/import/abstract_fountain_importer.h \ business_layer/import/abstract_importer.h \ business_layer/import/abstract_markdown_importer.h \ + business_layer/import/abstract_qtextdocument_importer.h \ business_layer/import/audioplay/abstract_audioplay_importer.h \ business_layer/import/audioplay/audioplay_fountain_importer.h \ business_layer/import/comic_book/abstract_comic_book_importer.h \