File indexing completed on 2024-05-12 15:55:08

0001 /************************************************************************
0002  *                                  *
0003  *  This file is part of Kooka, a scanning/OCR application using    *
0004  *  Qt <http://www.qt.io> and KDE Frameworks <http://www.kde.org>.  *
0005  *                                  *
0006  *  Copyright (C) 2020      Jonathan Marten <jjm@keelhaul.me.uk>    *
0007  *                                  *
0008  *  Kooka is free software; you can redistribute it and/or modify it    *
0009  *  under the terms of the GNU Library General Public License as    *
0010  *  published by the Free Software Foundation and appearing in the  *
0011  *  file COPYING included in the packaging of this file;  either    *
0012  *  version 2 of the License, or (at your option) any later version.    *
0013  *                                  *
0014  *  As a special exception, permission is given to link this program    *
0015  *  with any version of the KADMOS OCR/ICR engine (a product of     *
0016  *  reRecognition GmbH, Kreuzlingen), and distribute the resulting  *
0017  *  executable without including the source code for KADMOS in the  *
0018  *  source distribution.                        *
0019  *                                  *
0020  *  This program is distributed in the hope that it will be useful, *
0021  *  but WITHOUT ANY WARRANTY; without even the implied warranty of  *
0022  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the   *
0023  *  GNU General Public License for more details.            *
0024  *                                  *
0025  *  You should have received a copy of the GNU General Public       *
0026  *  License along with this program;  see the file COPYING.  If     *
0027  *  not, see <http://www.gnu.org/licenses/>.                *
0028  *                                  *
0029  ************************************************************************/
0030 
0031 #include "ocrtesseractdialog.h"
0032 
0033 #include <qlabel.h>
0034 #include <qregexp.h>
0035 #include <qcombobox.h>
0036 #include <qlayout.h>
0037 #include <qprogressbar.h>
0038 
0039 #include <klocalizedstring.h>
0040 #include <kurlrequester.h>
0041 #include <kprocess.h>
0042 
0043 #include "kookapref.h"
0044 #include "kookasettings.h"
0045 #include "dialogbase.h"
0046 
0047 #include "ocrtesseractengine.h"
0048 #include "ocr_logging.h"
0049 
0050 
0051 OcrTesseractDialog::OcrTesseractDialog(AbstractOcrEngine *plugin, QWidget *pnt)
0052     : AbstractOcrDialogue(plugin, pnt),
0053       m_setupWidget(nullptr),
0054       m_ocrCmd(QString()),
0055       m_versionNum(0),
0056       m_versionStr(QString())
0057 {
0058 }
0059 
0060 
0061 bool OcrTesseractDialog::setupGui()
0062 {
0063     AbstractOcrDialogue::setupGui();            // build the standard GUI
0064 
0065     // Options available vary with the Tesseract version.  So we need to find
0066     // the Tesseract binary and get its version before creating the GUI.
0067     m_ocrCmd = engine()->findExecutable(&KookaSettings::ocrTesseractBinary, KookaSettings::self()->ocrTesseractBinaryItem());
0068 
0069     if (!m_ocrCmd.isEmpty()) getVersion(m_ocrCmd);  // found, get its version
0070     else                        // not found or invalid
0071     {
0072         engine()->setErrorText(i18n("The Tesseract executable is not configured or is not available."));
0073     }
0074 
0075     QWidget *w = addExtraSetupWidget();
0076     QGridLayout *gl = new QGridLayout(w);
0077     int row = 0;
0078 
0079     // Language, auto detected values
0080     QMap<QString,QString> vals = getValidValues("list-langs");
0081     KConfigSkeletonItem *ski = KookaSettings::self()->ocrTesseractLanguageItem();
0082     Q_ASSERT(ski!=nullptr);
0083     QLabel *l = new QLabel(ski->label(), w);
0084     gl->addWidget(l, row, 0);
0085     m_language = new QComboBox(w);
0086     m_language->setToolTip(ski->toolTip());
0087     m_language->addItem(i18n("(default)"), QString());
0088     for (QMap<QString,QString>::const_iterator it = vals.constBegin(); it!=vals.constEnd(); ++it)
0089     {
0090         m_language->addItem((!it.value().isEmpty() ? it.value() : it.key()), it.key());
0091     }
0092  
0093     if (vals.isEmpty()) m_language->setEnabled(false);
0094     else
0095     {
0096         int ix = m_language->findData(KookaSettings::ocrTesseractLanguage());
0097         if (ix!=-1) m_language->setCurrentIndex(ix);
0098     }
0099 
0100     gl->addWidget(m_language, row, 1);
0101     l->setBuddy(m_language);
0102     ++row;
0103 
0104     // User words, from file
0105     ski = KookaSettings::self()->ocrTesseractUserWordsItem();
0106     Q_ASSERT(ski!=nullptr);
0107     l = new QLabel(ski->label(), w);
0108     gl->addWidget(l, row, 0);
0109     m_userWords = new KUrlRequester(w);
0110     m_userWords->setAcceptMode(QFileDialog::AcceptOpen);
0111     m_userWords->setMode(KFile::File|KFile::ExistingOnly|KFile::LocalOnly);
0112     m_userWords->setPlaceholderText(i18n("Select a file if required..."));
0113 
0114     QUrl u = KookaSettings::ocrTesseractUserWords();
0115     if (u.isValid()) m_userWords->setUrl(u);
0116 
0117     gl->addWidget(m_userWords, row, 1);
0118     l->setBuddy(m_userWords);
0119     ++row;
0120 
0121     // User patterns, from file
0122     ski = KookaSettings::self()->ocrTesseractUserPatternsItem();
0123     Q_ASSERT(ski!=nullptr);
0124     l = new QLabel(ski->label(), w);
0125     gl->addWidget(l, row, 0);
0126     m_userPatterns = new KUrlRequester(w);
0127     m_userPatterns->setAcceptMode(QFileDialog::AcceptOpen);
0128     m_userPatterns->setMode(KFile::File|KFile::ExistingOnly|KFile::LocalOnly);
0129     m_userPatterns->setPlaceholderText(i18n("Select a file if required..."));
0130 
0131     u = KookaSettings::ocrTesseractUserPatterns();
0132     if (u.isValid()) m_userPatterns->setUrl(u);
0133 
0134     gl->addWidget(m_userPatterns, row, 1);
0135     l->setBuddy(m_userPatterns);
0136     ++row;
0137 
0138     gl->setRowMinimumHeight(row, DialogBase::verticalSpacing());
0139     ++row;
0140 
0141     // Page segmentation mode, auto detected values
0142     vals = getValidValues("help-psm");
0143     ski = KookaSettings::self()->ocrTesseractSegmentationModeItem();
0144     Q_ASSERT(ski!=nullptr);
0145     l = new QLabel(ski->label(), w);
0146     gl->addWidget(l, row, 0);
0147     m_segmentationMode = new QComboBox(w);
0148     m_segmentationMode->setToolTip(ski->toolTip());
0149     m_segmentationMode->addItem(i18n("(default)"), QString());
0150     for (QMap<QString,QString>::const_iterator it = vals.constBegin(); it != vals.constEnd(); ++it)
0151     {
0152         m_segmentationMode->addItem(it.value(), it.key());
0153     }
0154  
0155     if (vals.isEmpty()) m_segmentationMode->setEnabled(false);
0156     else
0157     {
0158         int ix = m_segmentationMode->findData(KookaSettings::ocrTesseractSegmentationMode());
0159         if (ix!=-1) m_segmentationMode->setCurrentIndex(ix);
0160     }
0161 
0162     gl->addWidget(m_segmentationMode, row, 1);
0163     l->setBuddy(m_segmentationMode);
0164     ++row;
0165 
0166     // OCR engine mode, auto detected values
0167     vals = getValidValues("help-oem");
0168     ski = KookaSettings::self()->ocrTesseractEngineModeItem();
0169     Q_ASSERT(ski!=nullptr);
0170     l = new QLabel(ski->label(), w);
0171     gl->addWidget(l, row, 0);
0172     m_engineMode = new QComboBox(w);
0173     m_engineMode->setToolTip(ski->toolTip());
0174     m_engineMode->addItem(i18n("(default)"), QString());
0175     for (QMap<QString,QString>::const_iterator it = vals.constBegin(); it != vals.constEnd(); ++it)
0176     {
0177         m_engineMode->addItem(it.value(), it.key());
0178     }
0179  
0180     if (vals.isEmpty()) m_engineMode->setEnabled(false);
0181     else
0182     {
0183         int ix = m_engineMode->findData(KookaSettings::ocrTesseractEngineMode());
0184         if (ix!=-1) m_engineMode->setCurrentIndex(ix);
0185     }
0186 
0187     gl->addWidget(m_engineMode, row, 1);
0188     l->setBuddy(m_engineMode);
0189     ++row;
0190 
0191     gl->setRowMinimumHeight(row, DialogBase::verticalSpacing());
0192     ++row;
0193 
0194     gl->setRowStretch(row-1, 1);            // for top alignment
0195     gl->setColumnStretch(1, 1);
0196 
0197     ocrShowInfo(m_ocrCmd, m_versionStr);        // show the binary and version
0198     progressBar()->setRange(0, 0);          // progress animation only
0199 
0200     m_setupWidget = w;
0201     return (!m_ocrCmd.isEmpty());
0202 }
0203 
0204 
0205 void OcrTesseractDialog::slotWriteConfig()
0206 {
0207     AbstractOcrDialogue::slotWriteConfig();
0208 
0209     KookaSettings::setOcrTesseractBinary(getOCRCmd());
0210 
0211     KookaSettings::setOcrTesseractLanguage(m_language->currentData().toString());
0212     KookaSettings::setOcrTesseractUserWords(m_userWords->url());
0213     KookaSettings::setOcrTesseractUserPatterns(m_userPatterns->url());
0214     KookaSettings::setOcrTesseractSegmentationMode(m_segmentationMode->currentData().toString());
0215     KookaSettings::setOcrTesseractEngineMode(m_engineMode->currentData().toString());
0216 }
0217 
0218 
0219 void OcrTesseractDialog::enableFields(bool enable)
0220 {
0221     m_setupWidget->setEnabled(enable);
0222 }
0223 
0224 
0225 void OcrTesseractDialog::getVersion(const QString &bin)
0226 {
0227     qCDebug(OCR_LOG) << "of" << bin;
0228     if (bin.isEmpty()) return;
0229 
0230     KProcess proc;
0231     proc.setOutputChannelMode(KProcess::MergedChannels);
0232     proc << bin << "-v";
0233 
0234     int status = proc.execute(5000);
0235     if (status==0)
0236     {
0237         QByteArray output = proc.readAllStandardOutput();
0238         QRegExp rx("tesseract ([\\d\\.]+)");
0239         if (rx.indexIn(output)>-1)
0240         {
0241             m_ocrCmd = bin;
0242             m_versionStr = rx.cap(1);
0243             m_versionNum = m_versionStr.left(1).toInt();
0244             qCDebug(OCR_LOG) << "version" << m_versionStr << "=" << m_versionNum;
0245         }
0246     }
0247     else
0248     {
0249         qCDebug(OCR_LOG) << "failed with status" << status;
0250         m_versionStr = i18n("Error");
0251     }
0252 }
0253 
0254 
0255 QMap<QString,QString> OcrTesseractDialog::getValidValues(const QString &opt)
0256 {
0257     // The values displayed by and passed to Tesseract for OEM and PSM are integers,
0258     // but the values for the language are strings.  For simplicity we treat the
0259     // OEM/PSM settings as strings also.
0260     QMap<QString,QString> result;
0261 
0262     KConfigSkeletonItem *ski = KookaSettings::self()->ocrTesseractValidValuesItem();
0263     Q_ASSERT(ski!=nullptr);
0264     QString groupName = QString("%1_v%2").arg(ski->group()).arg(m_versionStr);
0265     KConfigGroup grp = KookaSettings::self()->config()->group(groupName);
0266 
0267     if (grp.hasKey(opt+"_keys"))            // values in config already
0268     {
0269         qCDebug(OCR_LOG) << "option" << opt << "already in config";
0270 
0271         const QStringList keys = grp.readEntry(opt+"_keys", QStringList());
0272         const QStringList descs = grp.readEntry(opt+"_descs", QStringList());
0273         for (int i = 0; i<keys.count(); ++i) result.insert(keys.at(i), descs.at(i));
0274     }
0275     else                        // not in config, need to extract
0276     {
0277         if (m_ocrCmd.isEmpty())
0278         {
0279             qCWarning(OCR_LOG) << "cannot get values, no binary";
0280             return (result);
0281         }
0282 
0283         KProcess proc;
0284         proc.setOutputChannelMode(KProcess::MergedChannels);
0285         proc << m_ocrCmd << QString("--%1").arg(opt);
0286 
0287         proc.execute(5000);
0288         const QByteArray output = proc.readAllStandardOutput();
0289         const QList<QByteArray> lines = output.split('\n');
0290         for (const QByteArray &line : lines)
0291         {
0292             const QString lineStr = QString::fromLocal8Bit(line);
0293             qCDebug(OCR_LOG) << "line:" << lineStr;
0294 
0295             QRegExp rx;
0296             if (opt=="list-langs") rx.setPattern("^\\s*(\\w+)()$");
0297             else rx.setPattern("^\\s*(\\d+)\\s+(\\w.+)?$");
0298             if (rx.indexIn(lineStr)>-1)
0299             {
0300                 const QString value = rx.cap(1);
0301                 QString desc = rx.cap(2).simplified();
0302                 if (desc.endsWith(QLatin1Char('.')) || desc.endsWith(QLatin1Char(','))) desc.chop(1);
0303                 result.insert(value, desc);
0304             }
0305         }
0306 
0307         qCDebug(OCR_LOG) << "parsed result count" << result.count();
0308         if (!result.isEmpty())
0309         {                           // save result for next time
0310             grp.writeEntry(opt+"_keys", result.keys());     // ordered list of keys
0311             grp.writeEntry(opt+"_descs", result.values());  // same-ordered list of values
0312             grp.sync();
0313         }
0314     }
0315 
0316     qCDebug(OCR_LOG) << "values for" << opt << "=" << result.keys();
0317     return (result);
0318 }