Skip to content

Commit

Permalink
Import documents from PDF
Browse files Browse the repository at this point in the history
Add import characters and locations from PDF
  • Loading branch information
iljukhaput authored and dimkanovikov committed Sep 9, 2024
1 parent 9af222a commit 7936873
Show file tree
Hide file tree
Showing 6 changed files with 364 additions and 285 deletions.
280 changes: 220 additions & 60 deletions src/corelib/business_layer/import/abstract_document_importer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,11 @@

#include <QRegularExpression>
#include <QTextBlock>
#include <QTextDocument>
#include <QXmlStreamWriter>

#include <set>


namespace BusinessLayer {

Expand All @@ -21,6 +24,12 @@ namespace {
const QRegularExpression kPlaceContainsChecker(
"^(INT|EXT|INT/EXT|ИНТ|НАТ|ИНТ/НАТ|ПАВ|ЭКСТ|ИНТ/ЭКСТ)([.]|[ - ])");

/**
* @brief Регулярное выражение для определения строки, начинающейся с номера
*/
const QRegularExpression kStartFromNumberChecker(
"^([\\d]{1,}[\\d\\S]{0,})([.]|[-])(([\\d\\S]{1,})([.]|)|) ");

/**
* @brief Регулярное выражение для определения блока "Титр" по наличию ключевых слов
*/
Expand Down Expand Up @@ -66,6 +75,138 @@ AbstractDocumentImporter::AbstractDocumentImporter()
AbstractDocumentImporter::~AbstractDocumentImporter() = default;


AbstractImporter::Documents AbstractDocumentImporter::importDocuments(
const ImportOptions& _options) const
{
//
// Преобразовать заданный документ в QTextDocument
//
QTextDocument document;
const bool documentDone = documentForImport(_options.filePath, document);
if (!documentDone) {
return {};
}

//
// Найти минимальный отступ слева для всех блоков
// ЗАЧЕМ: во многих программах (Final Draft, Screeviner) сделано так, что поля
// задаются за счёт оступов. Получается что и заглавие сцены и описание действия
// имеют отступы. Так вот это и будет минимальным отступом, который не будем считать
//
int minLeftMargin = 1000;
{
QTextCursor cursor(&document);
while (!cursor.atEnd()) {
if (minLeftMargin > cursor.blockFormat().leftMargin()) {
minLeftMargin = std::max(0.0, cursor.blockFormat().leftMargin());
}

cursor.movePosition(QTextCursor::NextBlock);
cursor.movePosition(QTextCursor::EndOfBlock);
}
}

QTextCursor cursor(&document);

//
// Для каждого блока текста определяем тип
//
// ... последний стиль блока
auto lastBlockType = TextParagraphType::Undefined;
// ... количество пустых строк
int emptyLines = 0;
std::set<QString> characterNames;
std::set<QString> locationNames;
do {
cursor.movePosition(QTextCursor::EndOfBlock);

//
// Если в блоке есть текст
//
if (!cursor.block().text().simplified().isEmpty()) {
//
// ... определяем тип
//
const auto blockType
= typeForTextCursor(cursor, lastBlockType, emptyLines, minLeftMargin);
QString paragraphText = cursor.block().text().simplified();

//
// Если текущий тип "Время и место", то удалим номер сцены
//
if (blockType == TextParagraphType::SceneHeading) {
paragraphText = TextHelper::smartToUpper(paragraphText);
const auto match = kStartFromNumberChecker.match(paragraphText);
if (match.hasMatch()) {
paragraphText = paragraphText.mid(match.capturedEnd());
}
}

//
// Выполняем корректировки
//
paragraphText = clearBlockText(blockType, paragraphText);

switch (blockType) {
case TextParagraphType::SceneHeading: {
if (!_options.importLocations) {
break;
}

const auto currentLocationName = locationName(paragraphText);
if (currentLocationName.isEmpty()) {
break;
}

locationNames.emplace(currentLocationName);
break;
}

case TextParagraphType::Character: {
if (!_options.importCharacters) {
break;
}

const auto currentCharacterName = characterName(paragraphText);
if (currentCharacterName.isEmpty()) {
break;
}

characterNames.emplace(currentCharacterName);
break;
}

default:
break;
}

//
// Запомним последний стиль блока и обнулим счётчик пустых строк
//
lastBlockType = blockType;
emptyLines = 0;
}
//
// Если в блоке нет текста, то увеличиваем счётчик пустых строк
//
else {
++emptyLines;
}

cursor.movePosition(QTextCursor::NextCharacter);
} while (!cursor.atEnd());

Documents documents;
for (const auto& characterName : characterNames) {
documents.characters.append(
{ Domain::DocumentObjectType::Character, characterName, {}, {} });
}
for (const auto& locationName : locationNames) {
documents.locations.append({ Domain::DocumentObjectType::Location, locationName, {}, {} });
}
return documents;
}

QString AbstractDocumentImporter::parseDocument(const ImportOptions& _options,
QTextDocument& _document) const
{
Expand Down Expand Up @@ -118,11 +259,27 @@ QString AbstractDocumentImporter::parseDocument(const ImportOptions& _options,
= typeForTextCursor(cursor, lastBlockType, emptyLines, minLeftMargin);

//
// Обработаем блок заголовка сцены в наследниках
// Извлечем номер сцены
//
QString sceneNumber;
if (blockType == TextParagraphType::SceneHeading) {
sceneNumber = extractSceneNumber(_options, cursor);
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 (shouldKeepSceneNumbers(_options)) {
sceneNumber = cursor.selectedText().trimmed();
if (sceneNumber.endsWith('.')) {
sceneNumber.chop(1);
}
}
cursor.deleteChar();
}
cursor.movePosition(QTextCursor::EndOfBlock);
}
}

//
Expand Down Expand Up @@ -218,60 +375,6 @@ QString AbstractDocumentImporter::parseDocument(const ImportOptions& _options,
return { result };
}

QString AbstractDocumentImporter::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;
}

TextParagraphType AbstractDocumentImporter::typeForTextCursor(const QTextCursor& _cursor,
TextParagraphType _lastBlockType,
int _prevEmptyLines,
Expand Down Expand Up @@ -418,13 +521,70 @@ TextParagraphType AbstractDocumentImporter::typeForTextCursor(const QTextCursor&
return blockType;
}

QString AbstractDocumentImporter::extractSceneNumber(const ImportOptions& _options,
QTextCursor& _cursor) const
QString AbstractDocumentImporter::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;
}

QRegularExpression AbstractDocumentImporter::startFromNumberChecker() const
{
return kStartFromNumberChecker;
}

bool AbstractDocumentImporter::shouldKeepSceneNumbers(const ImportOptions& _options) const
{
Q_UNUSED(_options)
Q_UNUSED(_cursor)

return QString();
return false;
}

void AbstractDocumentImporter::writeReviewMarks(QXmlStreamWriter& _writer,
Expand Down
Loading

0 comments on commit 7936873

Please sign in to comment.