"Fossies" - the Fresh Open Source Software Archive

Member "gimagereader-3.4.0/qt/src/Recognizer.cc" (28 Jan 2022, 17157 Bytes) of package /linux/privat/gimagereader-3.4.0.tar.xz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) C and C++ source code syntax highlighting (style: standard) with prefixed line numbers and code folding option. Alternatively you can here view or download the uninterpreted source code file. For more information about "Recognizer.cc" see the Fossies "Dox" file reference documentation and the latest Fossies "Diffs" side-by-side code changes report: 3.3.1_vs_3.4.0.

    1 /* -*- Mode: C++; indent-tabs-mode: t; c-basic-offset: 4; tab-width: 4 -*-  */
    2 /*
    3  * Recognizer.hh
    4  * Copyright (C) 2013-2022 Sandro Mani <manisandro@gmail.com>
    5  *
    6  * gImageReader is free software: you can redistribute it and/or modify it
    7  * under the terms of the GNU General Public License as published by the
    8  * Free Software Foundation, either version 3 of the License, or
    9  * (at your option) any later version.
   10  *
   11  * gImageReader is distributed in the hope that it will be useful, but
   12  * WITHOUT ANY WARRANTY; without even the implied warranty of
   13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
   14  * See the GNU General Public License for more details.
   15  *
   16  * You should have received a copy of the GNU General Public License along
   17  * with this program.  If not, see <http://www.gnu.org/licenses/>.
   18  */
   19 
   20 #include <QClipboard>
   21 #include <QDir>
   22 #include <QFileInfo>
   23 #include <QtSpell.hpp>
   24 #include <algorithm>
   25 #define USE_STD_NAMESPACE
   26 #include <tesseract/baseapi.h>
   27 #include <tesseract/ocrclass.h>
   28 #undef USE_STD_NAMESPACE
   29 #include <QMouseEvent>
   30 
   31 #ifdef Q_OS_WIN
   32 #include <fcntl.h>
   33 #define pipe(fds) _pipe(fds, 5000, _O_BINARY)
   34 #endif
   35 
   36 #include "ConfigSettings.hh"
   37 #include "Displayer.hh"
   38 #include "MainWindow.hh"
   39 #include "OutputEditor.hh"
   40 #include "RecognitionMenu.hh"
   41 #include "Recognizer.hh"
   42 #include "Utils.hh"
   43 
   44 class Recognizer::ProgressMonitor : public MainWindow::ProgressMonitor {
   45 public:
   46 #if TESSERACT_MAJOR_VERSION < 5
   47     ETEXT_DESC desc;
   48 #else
   49     tesseract::ETEXT_DESC desc;
   50 #endif
   51 
   52     ProgressMonitor(int nPages) : MainWindow::ProgressMonitor(nPages) {
   53         desc.progress = 0;
   54         desc.cancel = cancelCallback;
   55         desc.cancel_this = this;
   56     }
   57     int getProgress() const override {
   58         QMutexLocker locker(&mMutex);
   59         return 100.0 * ((mProgress + desc.progress / 100.0) / mTotal);
   60     }
   61     static bool cancelCallback(void* instance, int /*words*/) {
   62         ProgressMonitor* monitor = reinterpret_cast<ProgressMonitor*>(instance);
   63         QMutexLocker locker(&monitor->mMutex);
   64         return monitor->mCancelled;
   65     }
   66 };
   67 
   68 
   69 Recognizer::Recognizer(const UI_MainWindow& _ui) :
   70     ui(_ui) {
   71     QAction* currentPageAction = new QAction(_("Current Page"), this);
   72     currentPageAction->setData(static_cast<int>(PageSelection::Current));
   73 
   74     QAction* multiplePagesAction = new QAction(_("Multiple Pages..."), this);
   75     multiplePagesAction->setData(static_cast<int>(PageSelection::Multiple));
   76 
   77     m_actionBatchMode = new QAction(_("Batch mode..."), this);
   78     m_actionBatchMode->setData(static_cast<int>(PageSelection::Batch));
   79 
   80     m_menuPages = new QMenu(ui.toolButtonRecognize);
   81     m_menuPages->addAction(currentPageAction);
   82     m_menuPages->addAction(multiplePagesAction);
   83     m_menuPages->addAction(m_actionBatchMode);
   84 
   85     m_pagesDialog = new QDialog(MAIN);
   86     m_pagesDialogUi.setupUi(m_pagesDialog);
   87 
   88     m_batchDialog = new QDialog(MAIN);
   89     m_batchDialogUi.setupUi(m_batchDialog);
   90     m_batchDialogUi.comboBoxExisting->addItem(_("Overwrite existing output"), BatchOverwriteOutput);
   91     m_batchDialogUi.comboBoxExisting->addItem(_("Skip processing source"), BatchSkipSource);
   92 
   93     ui.toolButtonLanguages->setMenu(MAIN->getRecognitionMenu());
   94 
   95     connect(ui.toolButtonRecognize, &QToolButton::clicked, this, &Recognizer::recognizeButtonClicked);
   96     connect(currentPageAction, &QAction::triggered, this, &Recognizer::recognizeCurrentPage);
   97     connect(multiplePagesAction, &QAction::triggered, this, &Recognizer::recognizeMultiplePages);
   98     connect(m_actionBatchMode, &QAction::triggered, this, &Recognizer::recognizeBatch);
   99     connect(m_pagesDialogUi.lineEditPageRange, &QLineEdit::textChanged, this, &Recognizer::clearLineEditPageRangeStyle);
  100     connect(MAIN->getRecognitionMenu(), &RecognitionMenu::languageChanged, this, &Recognizer::recognitionLanguageChanged);
  101 
  102 
  103     ADD_SETTING(ComboSetting("ocrregionstrategy", m_pagesDialogUi.comboBoxRecognitionArea, 0));
  104     ADD_SETTING(SwitchSetting("ocraddsourcefilename", m_pagesDialogUi.checkBoxPrependFilename));
  105     ADD_SETTING(SwitchSetting("ocraddsourcepage", m_pagesDialogUi.checkBoxPrependPage));
  106 }
  107 
  108 void Recognizer::setRecognizeMode(const QString& mode) {
  109     m_modeLabel = mode;
  110     ui.toolButtonRecognize->setText(QString("%1\n%2").arg(m_modeLabel).arg(m_langLabel));
  111 }
  112 
  113 void Recognizer::recognitionLanguageChanged(const Config::Lang& lang) {
  114     if(!lang.code.isEmpty()) {
  115         m_langLabel = QString("%1 (%2)").arg(lang.name, lang.code);
  116     } else if(lang.name == "Multilingual") {
  117         m_langLabel = QString("%1").arg(lang.prefix);
  118     } else {
  119         m_langLabel = QString("%1").arg(lang.name);
  120     }
  121     ui.toolButtonRecognize->setText(QString("%1\n%2").arg(m_modeLabel).arg(m_langLabel));
  122 }
  123 
  124 void Recognizer::clearLineEditPageRangeStyle() {
  125     qobject_cast<QLineEdit*>(QObject::sender())->setStyleSheet("");
  126 }
  127 
  128 QList<int> Recognizer::selectPages(bool& autodetectLayout) {
  129     int nPages = MAIN->getDisplayer()->getNPages();
  130 
  131     m_pagesDialogUi.lineEditPageRange->setText(QString("1-%1").arg(nPages));
  132     m_pagesDialogUi.lineEditPageRange->setFocus();
  133     m_pagesDialogUi.labelRecognitionArea->setVisible(MAIN->getDisplayer()->allowAutodetectOCRAreas());
  134     m_pagesDialogUi.comboBoxRecognitionArea->setVisible(MAIN->getDisplayer()->allowAutodetectOCRAreas());
  135     m_pagesDialogUi.groupBoxPrepend->setVisible(MAIN->getDisplayer()->allowAutodetectOCRAreas());
  136 
  137     m_pagesDialogUi.comboBoxRecognitionArea->setItemText(0, MAIN->getDisplayer()->hasMultipleOCRAreas() ? _("Current selection") : _("Entire page"));
  138 
  139     QRegularExpression validateRegEx("^[\\d,\\-\\s]+$");
  140     QRegularExpressionMatch match;
  141     QList<int> pages;
  142     while(m_pagesDialog->exec() == QDialog::Accepted) {
  143         pages.clear();
  144         QString text = m_pagesDialogUi.lineEditPageRange->text();
  145         if((match = validateRegEx.match(text)).hasMatch()) {
  146             text.replace(QRegularExpression("\\s+"), "");
  147             for(const QString& block : text.split(',', Qt::SkipEmptyParts)) {
  148                 QStringList ranges = block.split('-', Qt::SkipEmptyParts);
  149                 if(ranges.size() == 1) {
  150                     int page = ranges[0].toInt();
  151                     if(page > 0 && page <= nPages) {
  152                         pages.append(page);
  153                     }
  154                 } else if(ranges.size() == 2) {
  155                     int start = std::max(1, ranges[0].toInt());
  156                     int end = std::min(nPages, ranges[1].toInt());
  157                     for(int page = start; page <= end; ++page) {
  158                         pages.append(page);
  159                     }
  160                 } else {
  161                     pages.clear();
  162                     break;
  163                 }
  164             }
  165         }
  166         if(pages.empty()) {
  167             m_pagesDialogUi.lineEditPageRange->setStyleSheet("background: #FF7777; color: #FFFFFF;");
  168         } else {
  169             break;
  170         }
  171     }
  172     std::sort(pages.begin(), pages.end());
  173     autodetectLayout = m_pagesDialogUi.comboBoxRecognitionArea->isVisible() ? m_pagesDialogUi.comboBoxRecognitionArea->currentIndex() == 1 : false;
  174     return pages;
  175 }
  176 
  177 void Recognizer::recognizeButtonClicked() {
  178     int nPages = MAIN->getDisplayer()->getNPages();
  179     if(nPages == 1) {
  180         recognize({MAIN->getDisplayer()->getCurrentPage()});
  181     } else {
  182         m_actionBatchMode->setVisible(MAIN->getDisplayer()->getNSources() > 1);
  183         ui.toolButtonRecognize->setCheckable(true);
  184         ui.toolButtonRecognize->setChecked(true);
  185         m_menuPages->popup(ui.toolButtonRecognize->mapToGlobal(QPoint(0, ui.toolButtonRecognize->height())));
  186         ui.toolButtonRecognize->setChecked(false);
  187         ui.toolButtonRecognize->setCheckable(false);
  188     }
  189 }
  190 
  191 void Recognizer::recognizeCurrentPage() {
  192     recognize({MAIN->getDisplayer()->getCurrentPage()});
  193 }
  194 
  195 void Recognizer::recognizeMultiplePages() {
  196     bool autodetectLayout = false;
  197     QList<int> pages = selectPages(autodetectLayout);
  198     recognize(pages, autodetectLayout);
  199 }
  200 
  201 std::unique_ptr<Utils::TesseractHandle> Recognizer::setupTesseract() {
  202     Config::Lang lang = MAIN->getRecognitionMenu()->getRecognitionLanguage();
  203     auto tess = std::unique_ptr<Utils::TesseractHandle>(new Utils::TesseractHandle(lang.prefix.toLocal8Bit().constData()));
  204     if(tess->get()) {
  205         tess->get()->SetPageSegMode(MAIN->getRecognitionMenu()->getPageSegmentationMode());
  206         tess->get()->SetVariable("tessedit_char_whitelist", MAIN->getRecognitionMenu()->getCharacterWhitelist().toLocal8Bit());
  207         tess->get()->SetVariable("tessedit_char_blacklist", MAIN->getRecognitionMenu()->getCharacterBlacklist().toLocal8Bit());
  208 #if TESSERACT_VERSION >= TESSERACT_MAKE_VERSION(5, 0, 0)
  209         tess->get()->SetVariable("thresholding_method", "1");
  210 #endif
  211     } else {
  212         QMessageBox::critical(MAIN, _("Recognition errors occurred"), _("Failed to initialize tesseract"));
  213     }
  214     return tess;
  215 }
  216 
  217 void Recognizer::recognize(const QList<int>& pages, bool autodetectLayout) {
  218     bool prependFile = pages.size() > 1 && ConfigSettings::get<SwitchSetting>("ocraddsourcefilename")->getValue();
  219     bool prependPage = pages.size() > 1 && ConfigSettings::get<SwitchSetting>("ocraddsourcepage")->getValue();
  220     auto tess = setupTesseract();
  221     if(!tess->get()) {
  222         return;
  223     }
  224     bool contains = false;
  225     for(int page : pages) {
  226         QString source;
  227         int sourcePage;
  228         if(MAIN->getDisplayer()->resolvePage(page, source, sourcePage)) {
  229             if(MAIN->getOutputEditor()->containsSource(source, sourcePage)) {
  230                 contains = true;
  231                 break;
  232             }
  233         }
  234     }
  235     if(contains) {
  236         if(QMessageBox::No == QMessageBox::question(MAIN, _("Source already recognized"), _("One or more selected sources were already recognized. Proceed anyway?"))) {
  237             return;
  238         }
  239     }
  240     QStringList errors;
  241     OutputEditor::ReadSessionData* readSessionData = MAIN->getOutputEditor()->initRead(*tess->get());
  242     ProgressMonitor monitor(pages.size());
  243     MAIN->showProgress(&monitor);
  244     MAIN->getDisplayer()->setBlockAutoscale(true);
  245     Utils::busyTask([&] {
  246         int npages = pages.size();
  247         int idx = 0;
  248         QString prevFile;
  249         for(int page : pages) {
  250             monitor.desc.progress = 0;
  251             ++idx;
  252             QMetaObject::invokeMethod(MAIN, "pushState", Qt::QueuedConnection, Q_ARG(MainWindow::State, MainWindow::State::Busy), Q_ARG(QString, _("Recognizing page %1 (%2 of %3)").arg(page).arg(idx).arg(npages)));
  253 
  254             PageData pageData;
  255             pageData.success = false;
  256             QMetaObject::invokeMethod(this, "setPage", Qt::BlockingQueuedConnection, Q_RETURN_ARG(PageData, pageData), Q_ARG(int, page), Q_ARG(bool, autodetectLayout));
  257             if(!pageData.success) {
  258                 errors.append(_("- Page %1: failed to render page").arg(page));
  259                 MAIN->getOutputEditor()->readError(_("\n[Failed to recognize page %1]\n"), readSessionData);
  260                 continue;
  261             }
  262             readSessionData->pageInfo = pageData.pageInfo;
  263             bool firstChunk = true;
  264             bool newFile = readSessionData->pageInfo.filename != prevFile;
  265             prevFile = readSessionData->pageInfo.filename;
  266             for(const QImage& image : pageData.ocrAreas) {
  267                 readSessionData->prependPage = prependPage && firstChunk;
  268                 readSessionData->prependFile = prependFile && (readSessionData->prependPage || newFile);
  269                 firstChunk = false;
  270                 newFile = false;
  271                 tess->get()->SetImage(image.bits(), image.width(), image.height(), 4, image.bytesPerLine());
  272                 tess->get()->SetSourceResolution(MAIN->getDisplayer()->getCurrentResolution());
  273                 tess->get()->Recognize(&monitor.desc);
  274                 if(!monitor.cancelled()) {
  275                     MAIN->getOutputEditor()->read(*tess->get(), readSessionData);
  276                 }
  277             }
  278             QMetaObject::invokeMethod(MAIN, "popState", Qt::QueuedConnection);
  279             monitor.increaseProgress();
  280             if(monitor.cancelled()) {
  281                 break;
  282             }
  283         }
  284         return true;
  285     }, _("Recognizing..."));
  286     MAIN->getDisplayer()->setBlockAutoscale(false);
  287     MAIN->hideProgress();
  288     MAIN->getOutputEditor()->finalizeRead(readSessionData);
  289     if(!errors.isEmpty()) {
  290         showRecognitionErrorsDialog(errors);
  291     }
  292 }
  293 
  294 void Recognizer::recognizeImage(const QImage& image, OutputDestination dest) {
  295     auto tess = setupTesseract();
  296     if(!tess->get()) {
  297         return;
  298     }
  299     tess->get()->SetImage(image.bits(), image.width(), image.height(), 4, image.bytesPerLine());
  300     ProgressMonitor monitor(1);
  301     MAIN->showProgress(&monitor);
  302     if(dest == OutputDestination::Buffer) {
  303         OutputEditor::ReadSessionData* readSessionData = MAIN->getOutputEditor()->initRead(*tess->get());
  304         readSessionData->pageInfo.filename = MAIN->getDisplayer()->getCurrentImage(readSessionData->pageInfo.page);
  305         readSessionData->pageInfo.angle = MAIN->getDisplayer()->getCurrentAngle();
  306         readSessionData->pageInfo.resolution = MAIN->getDisplayer()->getCurrentResolution();
  307         Utils::busyTask([&] {
  308             tess->get()->Recognize(&monitor.desc);
  309             if(!monitor.cancelled()) {
  310                 MAIN->getOutputEditor()->read(*tess->get(), readSessionData);
  311             }
  312             return true;
  313         }, _("Recognizing..."));
  314         MAIN->getOutputEditor()->finalizeRead(readSessionData);
  315     } else if(dest == OutputDestination::Clipboard) {
  316         QString output;
  317         if(Utils::busyTask([&] {
  318         tess->get()->Recognize(&monitor.desc);
  319             if(!monitor.cancelled()) {
  320                 char* text = tess->get()->GetUTF8Text();
  321                 output = QString::fromUtf8(text);
  322                 delete[] text;
  323                 return true;
  324             }
  325             return false;
  326         }, _("Recognizing..."))) {
  327             QApplication::clipboard()->setText(output);
  328         }
  329     }
  330     MAIN->hideProgress();
  331 }
  332 
  333 void Recognizer::recognizeBatch() {
  334     m_batchDialogUi.checkBoxPrependPage->setVisible(MAIN->getDisplayer()->allowAutodetectOCRAreas());
  335     m_batchDialogUi.checkBoxAutolayout->setVisible(MAIN->getDisplayer()->allowAutodetectOCRAreas());
  336     if(m_batchDialog->exec() != QDialog::Accepted) {
  337         return;
  338     }
  339     BatchExistingBehaviour existingBehaviour = static_cast<BatchExistingBehaviour>(m_batchDialogUi.comboBoxExisting->currentData().toInt());
  340     bool prependPage = MAIN->getDisplayer()->allowAutodetectOCRAreas() && m_batchDialogUi.checkBoxPrependPage->isChecked();
  341     bool autolayout = MAIN->getDisplayer()->allowAutodetectOCRAreas() && m_batchDialogUi.checkBoxAutolayout->isChecked();
  342     int nPages = MAIN->getDisplayer()->getNPages();
  343 
  344     auto tess = setupTesseract();
  345     if(!tess->get()) {
  346         return;
  347     }
  348 
  349     QMap<QString, QVariant> batchOptions;
  350     batchOptions["prependPage"] = prependPage;
  351     OutputEditor::BatchProcessor* batchProcessor = MAIN->getOutputEditor()->createBatchProcessor(batchOptions);
  352 
  353     QStringList errors;
  354     ProgressMonitor monitor(nPages);
  355     MAIN->showProgress(&monitor);
  356     MAIN->getDisplayer()->setBlockAutoscale(true);
  357     Utils::busyTask([&] {
  358         int idx = 0;
  359         QString currFilename;
  360         QFile outputFile;
  361         for(int page = 1; page <= nPages; ++page) {
  362             monitor.desc.progress = 0;
  363             ++idx;
  364             QMetaObject::invokeMethod(MAIN, "pushState", Qt::QueuedConnection, Q_ARG(MainWindow::State, MainWindow::State::Busy), Q_ARG(QString, _("Recognizing page %1 (%2 of %3)").arg(page).arg(idx).arg(nPages)));
  365 
  366             PageData pageData;
  367             pageData.success = false;
  368             QMetaObject::invokeMethod(this, "setPage", Qt::BlockingQueuedConnection, Q_RETURN_ARG(PageData, pageData), Q_ARG(int, page), Q_ARG(bool, autolayout));
  369             if(!pageData.success) {
  370                 errors.append(_("- %1:%2: failed to render page").arg(QFileInfo(pageData.pageInfo.filename).fileName()).arg(page));
  371                 continue;
  372             }
  373             if(pageData.pageInfo.filename != currFilename) {
  374                 if(outputFile.isOpen()) {
  375                     batchProcessor->writeFooter(&outputFile);
  376                     outputFile.close();
  377                 }
  378                 currFilename = pageData.pageInfo.filename;
  379                 QFileInfo finfo(pageData.pageInfo.filename);
  380                 QString fileName = QDir(finfo.absolutePath()).absoluteFilePath(finfo.baseName() + batchProcessor->fileSuffix());
  381                 bool exists = QFileInfo(fileName).exists();
  382                 if(exists && existingBehaviour == BatchSkipSource) {
  383                     errors.append(_("- %1: output already exists, skipping").arg(finfo.fileName()));
  384                 } else {
  385                     outputFile.setFileName(fileName);
  386                     if(!outputFile.open(QIODevice::WriteOnly)) {
  387                         errors.append(_("- %1: failed to create output file").arg(finfo.fileName()).arg(page));
  388                     } else {
  389                         batchProcessor->writeHeader(&outputFile, tess->get(), pageData.pageInfo);
  390                     }
  391                 }
  392             }
  393             if(outputFile.isOpen()) {
  394                 bool firstChunk = true;
  395                 for(const QImage& image : pageData.ocrAreas) {
  396                     tess->get()->SetImage(image.bits(), image.width(), image.height(), 4, image.bytesPerLine());
  397                     tess->get()->SetSourceResolution(MAIN->getDisplayer()->getCurrentResolution());
  398                     tess->get()->Recognize(&monitor.desc);
  399 
  400                     if(!monitor.cancelled()) {
  401                         batchProcessor->appendOutput(&outputFile, tess->get(), pageData.pageInfo, firstChunk);
  402                     }
  403                     firstChunk = false;
  404                 }
  405             }
  406             QMetaObject::invokeMethod(MAIN, "popState", Qt::QueuedConnection);
  407             monitor.increaseProgress();
  408             if(monitor.cancelled()) {
  409                 break;
  410             }
  411         }
  412         if(outputFile.isOpen()) {
  413             batchProcessor->writeFooter(&outputFile);
  414             outputFile.close();
  415         }
  416         return true;
  417     }, _("Recognizing..."));
  418     MAIN->getDisplayer()->setBlockAutoscale(false);
  419     MAIN->hideProgress();
  420     if(!errors.isEmpty()) {
  421         showRecognitionErrorsDialog(errors);
  422     }
  423     delete batchProcessor;
  424 }
  425 
  426 Recognizer::PageData Recognizer::setPage(int page, bool autodetectLayout) {
  427     PageData pageData;
  428     pageData.success = MAIN->getDisplayer()->setup(&page);
  429     if(pageData.success) {
  430         if(autodetectLayout) {
  431             MAIN->getDisplayer()->autodetectOCRAreas();
  432         }
  433         pageData.pageInfo.filename = MAIN->getDisplayer()->getCurrentImage(pageData.pageInfo.page);
  434         pageData.pageInfo.angle = MAIN->getDisplayer()->getCurrentAngle();
  435         pageData.pageInfo.resolution = MAIN->getDisplayer()->getCurrentResolution();
  436         pageData.ocrAreas = MAIN->getDisplayer()->getOCRAreas();
  437     }
  438     return pageData;
  439 }
  440 
  441 void Recognizer::showRecognitionErrorsDialog(const QStringList& errors) {
  442     Utils::messageBox(MAIN, _("Recognition errors occurred"), _("The following errors occurred:"), errors.join("\n"), QMessageBox::Warning, QDialogButtonBox::Close);
  443 }