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

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) 2000-2016 Klaas Freitag <freitag@suse.de>     *
0007  *                          Jonathan Marten <jjm@keelhaul.me.uk>    *
0008  *                                  *
0009  *  Kooka is free software; you can redistribute it and/or modify it    *
0010  *  under the terms of the GNU Library General Public License as    *
0011  *  published by the Free Software Foundation and appearing in the  *
0012  *  file COPYING included in the packaging of this file;  either    *
0013  *  version 2 of the License, or (at your option) any later version.    *
0014  *                                  *
0015  *  As a special exception, permission is given to link this program    *
0016  *  with any version of the KADMOS OCR/ICR engine (a product of     *
0017  *  reRecognition GmbH, Kreuzlingen), and distribute the resulting  *
0018  *  executable without including the source code for KADMOS in the  *
0019  *  source distribution.                        *
0020  *                                  *
0021  *  This program is distributed in the hope that it will be useful, *
0022  *  but WITHOUT ANY WARRANTY; without even the implied warranty of  *
0023  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the   *
0024  *  GNU General Public License for more details.            *
0025  *                                  *
0026  *  You should have received a copy of the GNU General Public       *
0027  *  License along with this program;  see the file COPYING.  If     *
0028  *  not, see <http://www.gnu.org/licenses/>.                *
0029  *                                  *
0030  ************************************************************************/
0031 
0032 #include "ocrgocrengine.h"
0033 
0034 #ifdef HAVE_ERRNO_H
0035 #include <errno.h>
0036 #endif
0037 #ifdef HAVE_STRERROR
0038 #include <string.h>
0039 #endif
0040 
0041 #include <qregexp.h>
0042 #include <qfile.h>
0043 #include <qdir.h>
0044 #include <qtemporaryfile.h>
0045 #include <qtemporarydir.h>
0046 #include <qprocess.h>
0047 
0048 #include <klocalizedstring.h>
0049 #include <kpluginfactory.h>
0050 #include <kconfigskeleton.h>
0051 
0052 #include "imgsaver.h"
0053 #include "scanimage.h"
0054 #include "imageformat.h"
0055 #include "ocrgocrdialog.h"
0056 #include "executablepathdialogue.h"
0057 #include "kookasettings.h"
0058 #include "ocr_logging.h"
0059 
0060 
0061 K_PLUGIN_FACTORY_WITH_JSON(OcrGocrEngineFactory, "kookaocr-gocr.json", registerPlugin<OcrGocrEngine>();)
0062 #include "ocrgocrengine.moc"
0063 
0064 
0065 static const char *possibleResultFiles[] = { "out30.png", "out20.png", "out30.bmp", "out20.bmp", nullptr };
0066 
0067 
0068 OcrGocrEngine::OcrGocrEngine(QObject *pnt, const QVariantList &args)
0069     : AbstractOcrEngine(pnt, "OcrGocrEngine")
0070 {
0071     m_tempDir = nullptr;
0072     m_inputFile = QString();            // input image file
0073     m_resultFile = QString();           // OCR result text file
0074 }
0075 
0076 
0077 AbstractOcrDialogue *OcrGocrEngine::createOcrDialogue(AbstractOcrEngine *plugin, QWidget *pnt)
0078 {
0079     return (new OcrGocrDialog(plugin, pnt));
0080 }
0081 
0082 
0083 bool OcrGocrEngine::createOcrProcess(AbstractOcrDialogue *dia, ScanImage::Ptr img)
0084 {
0085     OcrGocrDialog *gocrDia = static_cast<OcrGocrDialog *>(dia);
0086     const QString cmd = gocrDia->getOCRCmd();
0087 
0088     const char *format;
0089     if (img->depth() == 1) {
0090         format = "PBM";    // B&W bitmap
0091     } else if (img->isGrayscale()) {
0092         format = "PGM";    // greyscale
0093     } else {
0094         format = "PPM";    // colour
0095     }
0096 
0097     // TODO: if the input file is local and is readable by GOCR,
0098     // can use it directly (but don't delete it afterwards!)
0099     m_inputFile = tempSaveImage(img, ImageFormat(format));
0100                             // save image to a temp file
0101     QProcess *proc = initOcrProcess();          // start process for OCR
0102     QStringList args;                   // arguments for process
0103                             // new unique temporary directory
0104     m_tempDir = new QTemporaryDir(QDir::tempPath()+"/ocrgocrdir_XXXXXX");
0105     proc->setWorkingDirectory(m_tempDir->path());   // run process in there
0106 
0107     if (!isBW())                    // Not a B&W image
0108     {
0109         args << "-l" << QString::number(gocrDia->getGraylevel());
0110     }
0111     args << "-s" << QString::number(gocrDia->getSpaceWidth());
0112     args << "-d" << QString::number(gocrDia->getDustsize());
0113     args << "-a" << QString::number(gocrDia->getCertainty());
0114 
0115     // The verbosity value is a bitfield.  1 means to print more information,
0116     // 32 means output a PNG result image (which is always requested).
0117     args << "-v" << (gocrDia->verboseDebug() ? "33" : "32");
0118 
0119     // TODO: use '-f' to output XML (with position and accuracy data)
0120 
0121     // Specify this explicitly, because "-" does not mean the same
0122     // as "/dev/stdout".  The former interleaves the progress output
0123     // with the OCR result text.  See GOCR's process_arguments()
0124     // in gocr.c and ini_progress() in progress.c.
0125     //
0126     // This is still not very useful because GOCR only outputs progress
0127     // information every 10 seconds with no option for a shorter interval.
0128     // Set by 'time_t printinterval = 10' in GOCR's progress.c.
0129     //
0130     // Even for OCR processing which takes longer than that, it is still
0131     // not very useful because Kooka doesn't use the progress result - nothing
0132     // connects to the ocrProgress() signal :-(
0133     args << "-x" << "/dev/stdout";          // progress to stdout
0134 
0135     m_resultFile = tempFileName("gocrout.txt");     // OCR result text file
0136     args << "-o" << QFile::encodeName(m_resultFile);
0137 
0138     args << "-i" << QFile::encodeName(m_inputFile); // input image file
0139 
0140     proc->setProgram(cmd);
0141     proc->setArguments(args);
0142 
0143     proc->setReadChannel(QProcess::StandardOutput); // collect stdout for progress
0144     connect(proc, &QProcess::readyReadStandardOutput, this, &OcrGocrEngine::slotGOcrStdout);
0145 
0146     return (runOcrProcess());
0147 }
0148 
0149 
0150 bool OcrGocrEngine::finishedOcrProcess(QProcess *proc)
0151 {
0152     QFile rf(m_resultFile);
0153     if (!rf.open(QIODevice::ReadOnly)) {
0154 #ifdef HAVE_STRERROR
0155         const char *reason = strerror(errno);
0156 #else
0157         const char *reason = "";
0158 #endif
0159         setErrorText(xi18nc("@info", "Cannot read GOCR result file <filename>%1</filename><nl/>%2", m_resultFile, reason));
0160         return (false);
0161     }
0162 
0163     qCDebug(OCR_LOG) << "reading" << m_resultFile;
0164     const QString ocrResultText = rf.readAll();         // read all the result text
0165     rf.close();                     // finished with result file
0166 
0167     // Now all the text output by GOCR is in m_ocrResultText. Split this up
0168     // first into lines and then into words, and save this as the OCR results.
0169 #if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
0170     QStringList lines = ocrResultText.split('\n', Qt::SkipEmptyParts);
0171 #else
0172     QStringList lines = ocrResultText.split('\n', QString::SkipEmptyParts);
0173 #endif
0174     //qCDebug(OCR_LOG) << "RESULT" << ocrResultText;
0175     qCDebug(OCR_LOG) << "split to" << lines.count() << "lines";
0176 
0177     startResultDocument();
0178 
0179     for (QStringList::const_iterator itLine = lines.constBegin(); itLine != lines.constEnd(); ++itLine)
0180     {
0181         startLine();
0182 #if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
0183         QStringList words = (*itLine).split(QRegExp("\\s+"), Qt::SkipEmptyParts);
0184 #else
0185         QStringList words = (*itLine).split(QRegExp("\\s+"), QString::SkipEmptyParts);
0186 #endif
0187         for (QStringList::const_iterator itWord = words.constBegin(); itWord != words.constEnd(); ++itWord) {
0188             OcrWordData wd;
0189             addWord((*itWord), wd);
0190         }
0191         finishLine();
0192     }
0193 
0194     finishResultDocument();
0195     qCDebug(OCR_LOG) << "finished reading results";
0196 
0197     // Find the GOCR result image
0198     QDir dir(m_tempDir->path());
0199     QString foundResult;
0200     const char **prf = possibleResultFiles;
0201     while (*prf != nullptr) {              // search for result files
0202         QString ri = dir.absoluteFilePath(*prf);
0203         if (QFile::exists(ri)) {            // take first one that matches
0204             qCDebug(OCR_LOG) << "found result image" << ri;
0205             foundResult = ri;
0206             break;
0207         }
0208         ++prf;
0209     }
0210 
0211     // This used to replace the introduced image with the result file, having
0212     // been cleared above:
0213     //
0214     //   if (m_introducedImage!=nullptr) delete m_introducedImage;
0215     //   m_introducedImage = new ScanImage();
0216     //   ...
0217     //   if (!m_ocrResultFile.isNull()) m_introducedImage->load(m_ocrResultFile);
0218     //   else //qCDebug(OCR_LOG) << "cannot find result image in" << dir.absolutePath();
0219     //
0220     // But that seems pointless - the replaced m_introducedImage was not
0221     // subsequently used anywhere (although would it be reused if "Start OCR"
0222     // were done again without closing the dialogue?).  Not doing this means
0223     // that m_introducedImage can be const, which in turn means that it can
0224     // refer to the viewed image directly - i.e. there is no need to take a
0225     // copy in OcrEngine::setImage().  The result image is still displayed in
0226     // the image viewer in OcrEngine::finishedOCRVisible().
0227     if (!foundResult.isEmpty()) setResultImage(foundResult);
0228     else qCDebug(OCR_LOG) << "cannot find result image in" << dir.absolutePath();
0229 
0230     return (true);
0231 }
0232 
0233 
0234 QStringList OcrGocrEngine::tempFiles(bool retain)
0235 {
0236     QStringList result;
0237 
0238     result << m_inputFile << m_resultFile;
0239 
0240     if (m_tempDir != nullptr) {
0241         result << m_tempDir->path();
0242         m_tempDir->setAutoRemove(!retain);
0243         delete m_tempDir;               // autoRemove will do the rest
0244         m_tempDir = nullptr;
0245     }
0246 
0247     return (result);
0248 }
0249 
0250 void OcrGocrEngine::slotGOcrStdout()
0251 {
0252     // This never seems to match!  Format from an earlier GOCR version?
0253     QRegExp rx1("^\\s*(\\d+)\\s+(\\d+)");
0254 
0255     // GOCR 0.49 20100924 prints progress as:
0256     //
0257     //  progress pgm2asc_main   100 / 100  time[s]     7 /     7  (skip=63)
0258     //
0259     // Split up because we don't know what the 2nd field (counter name)
0260     // may contain.
0261     QRegExp rx2a("^\\s*progress ");
0262     QRegExp rx2b("\\s(\\d+)\\s+/\\s+(\\d+)\\s+time");
0263 
0264     int progress = -1;
0265     int subProgress;
0266 
0267     QByteArray line;
0268     while (!(line = ocrProcess()->readLine()).isEmpty()) {
0269         //qCDebug(OCR_LOG) << "GOCR stdout:" << line;
0270         // Calculate OCR progress
0271         if (rx1.indexIn(line) > -1) {
0272             progress = rx1.capturedTexts()[1].toInt();
0273             subProgress = rx1.capturedTexts()[2].toInt();
0274         } else if (rx2a.indexIn(line) > -1 && rx2b.indexIn(line) > -1) {
0275             progress = rx2b.capturedTexts()[1].toInt();
0276             subProgress = rx2b.capturedTexts()[2].toInt();
0277         }
0278 
0279         if (progress > 0) emit ocrProgress(progress, subProgress);
0280     }
0281 }
0282 
0283 
0284 void OcrGocrEngine::openAdvancedSettings()
0285 {
0286     ExecutablePathDialogue d(nullptr);
0287 
0288     QString exec = KookaSettings::ocrGocrBinary();
0289     if (exec.isEmpty())
0290     {
0291         KConfigSkeletonItem *ski = KookaSettings::self()->ocrGocrBinaryItem();
0292         ski->setDefault();
0293         exec = KookaSettings::ocrGocrBinary();
0294     }
0295 
0296     d.setPath(exec);
0297     d.setLabel(i18n("Name or path of the GOCR executable:"));
0298     if (!d.exec()) return;
0299 
0300     KookaSettings::setOcrGocrBinary(d.path());
0301 }