File indexing completed on 2024-04-28 15:39:40

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 "abstractocrengine.h"
0033 
0034 #include <qdir.h>
0035 #include <qfileinfo.h>
0036 #include <qprocess.h>
0037 #include <qtemporaryfile.h>
0038 
0039 #include <qtextdocument.h>
0040 #include <qtextcursor.h>
0041 
0042 #include <kwidgetsaddons_version.h>
0043 #include <kmessagebox.h>
0044 #include <klocalizedstring.h>
0045 #include <kcolorscheme.h>
0046 #include <kconfigskeleton.h>
0047 
0048 #include "imagecanvas.h"
0049 #include "imageformat.h"
0050 
0051 #include "abstractocrdialogue.h"
0052 #include "ocr_logging.h"
0053 
0054 
0055 //  Constructor/destructor and external engine creation
0056 //  ---------------------------------------------------
0057 
0058 AbstractOcrEngine::AbstractOcrEngine(QObject *pnt, const char *name)
0059     : AbstractPlugin(pnt),
0060       m_ocrProcess(nullptr),
0061       m_ocrRunning(false),
0062       m_ocrDialog(nullptr),
0063       m_imgCanvas(nullptr),
0064       m_document(nullptr),
0065       m_cursor(nullptr),
0066       m_currHighlight(-1),
0067       m_trackingActive(false)
0068 {
0069     setObjectName(name);
0070     qCDebug(OCR_LOG) << objectName();
0071 
0072     m_parent = nullptr;
0073 
0074     m_resolvedBW = false;               // have not examined image yet
0075 }
0076 
0077 
0078 AbstractOcrEngine::~AbstractOcrEngine()
0079 {
0080     qCDebug(OCR_LOG) << objectName();
0081     if (m_ocrProcess!=nullptr) delete m_ocrProcess;
0082     if (m_ocrDialog!=nullptr) delete m_ocrDialog;
0083 }
0084 
0085 
0086 /*
0087  * This is called to introduce a new image, usually if the user clicks on a
0088  * new image either in the gallery or on the thumbnailview.
0089  */
0090 void AbstractOcrEngine::setImage(ScanImage::Ptr img)
0091 {
0092     m_introducedImage = img;                // shared copy of original
0093     m_resolvedBW = false;               // invalidate previous result
0094 
0095     if (m_ocrDialog!=nullptr) m_ocrDialog->introduceImage(m_introducedImage);
0096     m_trackingActive = false;
0097 }
0098 
0099 
0100 /*
0101  * Starts the visual OCR process. Depending on the OCR engine, this function creates
0102  * a new dialog, and shows it.
0103  */
0104 bool AbstractOcrEngine::openOcrDialogue(QWidget *pnt)
0105 {
0106     if (m_ocrRunning) {
0107         KMessageBox::error(pnt, i18n("OCR is already in progress"));
0108         return (false);
0109     }
0110 
0111     m_parent = pnt;
0112     m_errorText.clear();                // ready for new messages
0113 
0114     m_ocrDialog = createOcrDialogue(this, pnt);
0115     Q_ASSERT(m_ocrDialog!=nullptr);
0116 
0117     if (!m_ocrDialog->setupGui())
0118     {
0119         const QString msg = collectErrorMessages(i18n("OCR could not be started."),
0120                                                  i18n("Check the OCR engine selection and settings."));
0121         int result = KMessageBox::warningContinueCancel(pnt, msg,
0122                                                         i18n("OCR Setup Error"),
0123                                                         KGuiItem(i18n("Configure OCR...")));
0124         if (result==KMessageBox::Continue) emit openOcrPrefs();
0125         return (false);                 // with no OCR dialogue
0126     }
0127 
0128     connect(m_ocrDialog, &AbstractOcrDialogue::signalOcrStart, this, &AbstractOcrEngine::slotStartOCR);
0129     connect(m_ocrDialog, &AbstractOcrDialogue::signalOcrStop, this, &AbstractOcrEngine::slotStopOCR);
0130     connect(m_ocrDialog, &QDialog::rejected, this, &AbstractOcrEngine::slotClose);
0131 
0132     m_ocrDialog->introduceImage(m_introducedImage);
0133     m_ocrDialog->show();
0134 
0135     // TODO: m_ocrActive would better reflect the function (if indeed useful at all)
0136     m_ocrRunning = true;
0137     return (true);
0138 }
0139 
0140 
0141 /* Called by "Close" used while OCR is not in progress */
0142 void AbstractOcrEngine::slotClose()
0143 {
0144     stopOcrProcess(false);
0145 }
0146 
0147 
0148 /* Called by "Stop" used while OCR is in progress */
0149 void AbstractOcrEngine::slotStopOCR()
0150 {
0151     Q_ASSERT(m_ocrDialog!=nullptr);
0152 
0153     stopOcrProcess(true);
0154     m_ocrDialog->enableGUI(false);          // enable controls again
0155 }
0156 
0157 
0158 /* Called by "Start" used while OCR is not in progress */
0159 void AbstractOcrEngine::slotStartOCR()
0160 {
0161     Q_ASSERT(m_ocrDialog!=nullptr);
0162 
0163     m_ocrDialog->enableGUI(true);           // disable controls while running
0164     m_ocrDialog->show();                // just in case it got closed
0165 
0166     createOcrProcess(m_ocrDialog, m_introducedImage);
0167 }
0168 
0169 
0170 void AbstractOcrEngine::stopOcrProcess(bool tellUser)
0171 {
0172     if (m_ocrProcess!=nullptr && m_ocrProcess->state()==QProcess::Running)
0173     {
0174         // If the process is to be killed, there is no point allowing
0175         // it to call slotProcessExited().  The finishedOcr() below
0176         // will clean up.
0177         disconnect(m_ocrProcess, nullptr, this, nullptr);
0178 
0179         qCDebug(OCR_LOG) << "Killing OCR process" << m_ocrProcess->processId();
0180         m_ocrProcess->kill();
0181         if (tellUser) KMessageBox::error(m_parent, i18n("The OCR process was stopped"));
0182     }
0183 
0184     finishedOcr(false);
0185 }
0186 
0187 
0188 /**
0189  * This method should be called by the engine specific finish slots.
0190  * It does the engine independent cleanups like re-enabling buttons etc.
0191  */
0192 void AbstractOcrEngine::finishedOcr(bool success)
0193 {
0194     if (m_ocrDialog!=nullptr) m_ocrDialog->enableGUI(false);
0195 
0196     if (success)
0197     {
0198         if (!m_ocrResultFile.isEmpty() &&       // there is a result image
0199             m_imgCanvas!=nullptr)           // and we can display it
0200         {
0201             // Load the result image from its file.
0202             // The QSharedPointer passed to ImageCanvas::newImage() will
0203             // retain the image and delete it when it is no longer needed.
0204             ScanImage *resultImage = new ScanImage(QUrl::fromLocalFile(m_ocrResultFile));
0205             qCDebug(OCR_LOG) << "Result image from" << m_ocrResultFile << "size" << resultImage->size();
0206             m_imgCanvas->newImage(ScanImage::Ptr(resultImage), true);
0207             m_imgCanvas->setReadOnly(true);     // display on image canvas
0208             m_trackingActive = true;            // handle clicks on image
0209         }
0210 
0211         // Do this after loading the result image above.  Then when the signal
0212         // reaches KookaView::slotOcrResultAvailable() which then calls
0213         // updateSelectionState(), the image canvas has the image loaded and
0214         // the image view actions are correctly enabled.
0215         emit newOCRResultText();            // send out the text result
0216 
0217         /* now it is time to invoke the dictionary if required */
0218         // TODO: readOnlyEditor needed here? Also done in finishResultDocument()
0219         emit readOnlyEditor(false);         // user can now edit
0220         if (m_ocrDialog != nullptr)
0221         {
0222             emit setSpellCheckConfig(m_ocrDialog->customSpellConfigFile());
0223 
0224             bool doSpellcheck = m_ocrDialog->wantInteractiveSpellCheck();
0225             bool bgSpellcheck = m_ocrDialog->wantBackgroundSpellCheck();
0226             emit startSpellCheck(doSpellcheck, bgSpellcheck);
0227         }
0228     }
0229 
0230     if (m_ocrDialog!=nullptr) m_ocrDialog->hide();  // close the dialogue
0231     m_ocrRunning = false;
0232     removeTempFiles();
0233 
0234     qCDebug(OCR_LOG) << "OCR finished";
0235 }
0236 
0237 
0238 void AbstractOcrEngine::removeTempFiles()
0239 {
0240     bool retain = m_ocrDialog->keepTempFiles();
0241     qCDebug(OCR_LOG) << "retain?" << retain;
0242 
0243     QStringList temps = tempFiles(retain);          // get files used by engine
0244     if (!m_ocrResultFile.isEmpty()) temps << m_ocrResultFile;   // plus our result image
0245     if (!m_ocrStderrLog.isEmpty()) temps << m_ocrStderrLog; // and our standard error log
0246     if (temps.join("").isEmpty()) return;           // no temporary files to remove
0247 
0248     if (retain)
0249     {
0250         QString s = xi18nc("@info", "The following OCR temporary files are retained for debugging:<nl/><nl/>");
0251         for (QStringList::const_iterator it = temps.constBegin(); it != temps.constEnd(); ++it)
0252         {
0253             const QString file = (*it);
0254             if (file.isEmpty()) continue;
0255 
0256             QUrl u = QUrl::fromLocalFile(file);
0257             s += xi18nc("@info", "<filename><link url=\"%1\">%2</link></filename><nl/>", u.url(), file);
0258         }
0259 
0260 #if KWIDGETSADDONS_VERSION >= QT_VERSION_CHECK(5, 100, 0)
0261         if (KMessageBox::questionTwoActions(m_parent, s,
0262 #else
0263         if (KMessageBox::questionYesNo(m_parent, s,
0264 #endif
0265                                        i18n("OCR Temporary Files"),
0266                                        KStandardGuiItem::del(),
0267                                        KStandardGuiItem::close(),
0268                                        QString(),
0269                                        KMessageBox::AllowLink)
0270 #if KWIDGETSADDONS_VERSION >= QT_VERSION_CHECK(5, 100, 0)
0271             == KMessageBox::PrimaryAction) {
0272 #else
0273             == KMessageBox::Yes) {
0274 #endif
0275             retain = false;
0276         }
0277     }
0278 
0279     if (!retain) {
0280         for (QStringList::const_iterator it = temps.constBegin(); it != temps.constEnd(); ++it) {
0281             if ((*it).isEmpty()) {
0282                 continue;
0283             }
0284 
0285             QString tf = (*it);
0286             QFileInfo fi(tf);
0287             if (!fi.exists()) {             // what happened?
0288                 //qCDebug(OCR_LOG) << "does not exist:" << tf;
0289             } else if (fi.isDir()) {
0290                 //qCDebug(OCR_LOG) << "temp dir" << tf;
0291                 QDir(tf).removeRecursively();       // recursive deletion
0292             } else {
0293                 //qCDebug(OCR_LOG) << "temp file" << tf;
0294                 QFile::remove(tf);          // just a simple file
0295             }
0296         }
0297     }
0298 }
0299 
0300 
0301 //  Filtering mouse events on the image viewer
0302 //  ------------------------------------------
0303 void AbstractOcrEngine::setImageCanvas(ImageCanvas *canvas)
0304 {
0305     m_imgCanvas = canvas;
0306     connect(m_imgCanvas, &ImageCanvas::doubleClicked, this, &AbstractOcrEngine::slotImagePosition);
0307 }
0308 
0309 
0310 void AbstractOcrEngine::slotImagePosition(const QPoint &p)
0311 {
0312     if (!m_trackingActive) return;          // not interested
0313 
0314     // ImageCanvas did the coordinate conversion.
0315     // OcrResEdit does all of the rest of the work.
0316     emit selectWord(p);
0317 }
0318 
0319 
0320 //  Highlighting/scrolling the result text
0321 //  --------------------------------------
0322 void AbstractOcrEngine::slotHighlightWord(const QRect &r)
0323 {
0324     if (m_imgCanvas == nullptr) {
0325         return;
0326     }
0327 
0328     if (m_currHighlight > -1) {
0329         m_imgCanvas->removeHighlight(m_currHighlight);
0330     }
0331     m_currHighlight = -1;
0332 
0333     if (!m_trackingActive) {
0334         return;    // not highlighting
0335     }
0336     if (!r.isValid()) {
0337         return;    // word rectangle invalid
0338     }
0339 
0340     KColorScheme sch(QPalette::Active, KColorScheme::Selection);
0341     QColor col = sch.background(KColorScheme::NegativeBackground).color();
0342     m_imgCanvas->setHighlightStyle(ImageCanvas::HighlightBox, QPen(col, 2));
0343     m_currHighlight = m_imgCanvas->addHighlight(r, true);
0344 }
0345 
0346 void AbstractOcrEngine::slotScrollToWord(const QRect &r)
0347 {
0348     if (m_imgCanvas == nullptr) {
0349         return;
0350     }
0351 
0352     if (m_currHighlight > -1) {
0353         m_imgCanvas->removeHighlight(m_currHighlight);
0354     }
0355     m_currHighlight = -1;
0356 
0357     if (!m_trackingActive) {
0358         return;    // not highlighting
0359     }
0360 
0361     KColorScheme sch(QPalette::Active, KColorScheme::Selection);
0362     QColor col = sch.background(KColorScheme::NeutralBackground).color();
0363     m_imgCanvas->setHighlightStyle(ImageCanvas::HighlightUnderline, QPen(col, 2));
0364     m_currHighlight = m_imgCanvas->addHighlight(r, true);
0365 }
0366 
0367 
0368 //  Assembling the OCR results
0369 //  --------------------------
0370 void AbstractOcrEngine::setTextDocument(QTextDocument *doc)
0371 {
0372     m_document = doc;
0373 }
0374 
0375 
0376 QTextDocument *AbstractOcrEngine::startResultDocument()
0377 {
0378     m_document->setUndoRedoEnabled(false);
0379     m_document->clear();
0380     m_wordCount = 0;
0381 
0382     m_cursor = new QTextCursor(m_document);
0383 
0384     emit readOnlyEditor(true);              // read only while updating
0385     return (m_document);
0386 }
0387 
0388 void AbstractOcrEngine::finishResultDocument()
0389 {
0390     qCDebug(OCR_LOG) << "words" << m_wordCount << "lines" << m_document->blockCount() << "chars" << m_document->characterCount();
0391 
0392     if (m_cursor != nullptr) delete m_cursor;
0393     emit readOnlyEditor(false);             // now let user edit it
0394 }
0395 
0396 void AbstractOcrEngine::startLine()
0397 {
0398     if (verboseDebug()) {
0399         qCDebug(OCR_LOG);
0400     }
0401     if (!m_cursor->atStart()) {
0402         m_cursor->insertBlock(QTextBlockFormat(), QTextCharFormat());
0403     }
0404 }
0405 
0406 void AbstractOcrEngine::finishLine()
0407 {
0408 }
0409 
0410 void AbstractOcrEngine::addWord(const QString &word, const OcrWordData &data)
0411 {
0412     if (verboseDebug()) {
0413         qCDebug(OCR_LOG) << "word" << word << "len" << word.length()
0414                          << "rect" << data.property(OcrWordData::Rectangle)
0415                          << "alts" << data.property(OcrWordData::Alternatives);
0416     }
0417 
0418     if (!m_cursor->atBlockStart()) {
0419         m_cursor->insertText(" ", QTextCharFormat());
0420     }
0421     m_cursor->insertText(word, data);
0422     ++m_wordCount;
0423 }
0424 
0425 
0426 QString AbstractOcrEngine::tempFileName(const QString &suffix, const QString &baseName)
0427 {
0428     QString protoName = QDir::tempPath()+'/'+baseName+"_XXXXXX";
0429     if (!suffix.isEmpty()) protoName += "."+suffix;
0430 
0431     QTemporaryFile tmpFile(protoName);
0432     tmpFile.setAutoRemove(false);
0433 
0434     if (!tmpFile.open())
0435     {
0436         qCDebug(OCR_LOG) << "error creating temporary file" << protoName;
0437         setErrorText(xi18nc("@info", "Cannot create temporary file <filename>%1</filename>", protoName));
0438         return (QString());
0439     }
0440 
0441     QString tmpName = QFile::encodeName(tmpFile.fileName());
0442     tmpFile.close();                    // just want its name
0443     return (tmpName);
0444 }
0445 
0446 
0447 QString AbstractOcrEngine::tempSaveImage(ScanImage::Ptr img, const ImageFormat &format, int colors)
0448 {
0449     if (img.isNull()) return (QString());       // no image to save
0450 
0451     QString tmpName = tempFileName(format.extension(), "imagetemp");
0452 
0453     ScanImage::Ptr tmpImg = img;            // original image
0454     if (colors!=-1 && img->depth()!=colors)     // need to convert it?
0455     {
0456         QImage::Format newfmt;
0457         switch (colors)
0458         {
0459 case 1:     newfmt = QImage::Format_Mono;
0460             break;
0461 
0462 case 8:     newfmt = QImage::Format_Indexed8;
0463             break;
0464 
0465 case 24:    newfmt = QImage::Format_RGB888;
0466             break;
0467 
0468 case 32:    newfmt = QImage::Format_RGB32;
0469             break;
0470 
0471 default:    qCWarning(OCR_LOG) << "bad colour depth" << colors;
0472             return (QString());
0473         }
0474 
0475         tmpImg.clear();                 // lose reference to original
0476         tmpImg.reset(new ScanImage(img->convertToFormat(newfmt)));
0477     }                           // update with converted image
0478 
0479     qCDebug(OCR_LOG) << "saving to" << tmpName << "in format" << format;
0480     if (!tmpImg->save(tmpName, format.name()))
0481     {
0482         qCDebug(OCR_LOG) << "error saving to" << tmpName;
0483         setErrorText(xi18nc("@info", "Cannot save image to temporary file <filename>%1</filename>", tmpName));
0484         tmpName.clear();
0485     }
0486 
0487     return (tmpName);
0488 }
0489 
0490 
0491 bool AbstractOcrEngine::verboseDebug() const
0492 {
0493     return (m_ocrDialog->verboseDebug());
0494 }
0495 
0496 
0497 QString AbstractOcrEngine::findExecutable(QString (*settingFunc)(), KConfigSkeletonItem *settingItem)
0498 {
0499     QString exec = (*settingFunc)();            // get current setting
0500     if (exec.isEmpty()) settingItem->setDefault();  // if null, apply default
0501     exec = (*settingFunc)();                // and get new setting
0502     Q_ASSERT(!exec.isEmpty());              // should now have something
0503     qCDebug(OCR_LOG) << "configured/default" << exec;
0504 
0505     if (!QDir::isAbsolutePath(exec))            // not specified absolute path
0506     {
0507         const QString pathExec = QStandardPaths::findExecutable(exec);
0508         if (pathExec.isEmpty())             // try to find executable
0509         {
0510             qCDebug(OCR_LOG) << "no" << exec << "found on PATH";
0511             setErrorText(xi18nc("@info", "The executable <command>%1</command> could not be found on <envar>PATH</envar>."));
0512             return (QString());
0513         }
0514         exec = pathExec;
0515     }
0516 
0517     QFileInfo fi(exec);                 // now check it is usable
0518     if (!fi.exists() || fi.isDir() || !fi.isExecutable())
0519     {
0520         qCDebug(OCR_LOG) << "configured" << exec << "not usable";
0521         setErrorText(xi18nc("@info", "The executable <filename>%1</filename> does not exist or is not usable.", fi.absoluteFilePath()));
0522         return (QString());
0523     }
0524 
0525     qCDebug(OCR_LOG) << "found" << exec;
0526     return (exec);
0527 }
0528 
0529 
0530 QString AbstractOcrEngine::collectErrorMessages(const QString &starter, const QString &ender)
0531 {
0532     // Any error message(s) in m_errorText will already have been converted
0533     // from KUIT markup to HTML by xi18nc() or similar.  So all that is
0534     // needed is to build the rest of the error message in rich text also.
0535     // There will be some spurious <html> tags around each separate message
0536     // which has been converted, but they don't seem to cause any problem.
0537 
0538     m_errorText.prepend(QString());
0539     m_errorText.prepend(starter);
0540     m_errorText.prepend("<html>");
0541 
0542     m_errorText.append(QString());
0543     m_errorText.append(ender);
0544     m_errorText.append("</html>");
0545 
0546     return (m_errorText.join("<br/>"));
0547 }
0548 
0549 
0550 QProcess *AbstractOcrEngine::initOcrProcess()
0551 {
0552     if (m_ocrProcess!=nullptr) delete m_ocrProcess; // kill old process if still there
0553 
0554     m_ocrProcess = new QProcess();          // start new OCR process
0555     Q_CHECK_PTR(m_ocrProcess);
0556     qCDebug(OCR_LOG);
0557 
0558     m_ocrProcess->setStandardInputFile(QProcess::nullDevice());
0559 
0560     m_ocrProcess->setProcessChannelMode(QProcess::SeparateChannels);
0561     m_ocrStderrLog = tempFileName("stderr.log");
0562     m_ocrProcess->setStandardErrorFile(m_ocrStderrLog);
0563 
0564     return (m_ocrProcess);
0565 }
0566 
0567 
0568 bool AbstractOcrEngine::runOcrProcess()
0569 {
0570     qCDebug(OCR_LOG) << "Running OCR," << m_ocrProcess->program() << m_ocrProcess->arguments();
0571     connect(m_ocrProcess, QOverload<int,QProcess::ExitStatus>::of(&QProcess::finished), this, &AbstractOcrEngine::slotProcessExited);
0572 
0573     m_ocrProcess->start();
0574     if (!m_ocrProcess->waitForStarted(5000))
0575     {
0576         qCWarning(OCR_LOG) << "Error starting OCR process";
0577         return (false);
0578     }
0579 
0580     return (true);
0581 }
0582 
0583 
0584 void AbstractOcrEngine::slotProcessExited(int exitCode, QProcess::ExitStatus exitStatus)
0585 {
0586     qCDebug(OCR_LOG) << "exit code" << exitCode << "status" << exitStatus;
0587 
0588     bool success = (exitStatus==QProcess::NormalExit && exitCode==0);
0589     if (!success)                   // OCR command failed
0590     {
0591         if (exitStatus==QProcess::CrashExit)
0592         {
0593             setErrorText(xi18nc("@info", "Command <command>%1</command> crashed with exit status <numid>%2</numid>",
0594                                 m_ocrProcess->program(), exitCode));
0595         }
0596         else
0597         {
0598             setErrorText(xi18nc("@info", "Command <command>%1</command> exited with status <numid>%2</numid>",
0599                                 m_ocrProcess->program(), exitCode));
0600         }
0601 
0602         const QString msg = collectErrorMessages(xi18nc("@info", "Running the OCR process failed."),
0603                                                  xi18nc("@info", "More information may be available in its <link url=\"%1\">standard error</link> log file.",
0604                                                         QUrl::fromLocalFile(m_ocrStderrLog).url()));
0605 
0606         KMessageBox::error(m_parent, msg, i18n("OCR Command Failed"), KMessageBox::AllowLink);
0607     }
0608     else                        // OCR command succeeded
0609     {
0610         success = finishedOcrProcess(m_ocrProcess); // process the OCR results
0611         if (!success)                   // OCR processing failed
0612         {
0613             const QString msg = collectErrorMessages(xi18nc("@info", "Processing the OCR results failed."), QString());
0614             KMessageBox::error(m_parent, msg, i18n("OCR Processing Failed"), KMessageBox::AllowLink);
0615         }
0616     }
0617 
0618     finishedOcr(success);
0619 }
0620 
0621 
0622 bool AbstractOcrEngine::isBW()
0623 {
0624     if (m_resolvedBW) return (m_isBW);          // have already resolved this
0625 
0626     m_isBW = false;                 // assume so to start
0627 
0628     const int cols = m_introducedImage->colorCount();   // colour count for indexed images
0629     if (cols>0)                     // not a true colour image
0630     {
0631         m_isBW = (cols<=2);             // only this many palette entries
0632     }
0633     else                        // image is true colour
0634     {
0635         if (m_introducedImage->allGray())       // could possibly be BW
0636         {
0637             int othercols = 0;              // colours not white or black
0638 
0639             const int w = m_introducedImage->width();
0640             const int h = m_introducedImage->height();
0641             for (int x = 0; x<w; ++x)
0642             {
0643                 for (int y = 0; y<h; ++y)
0644                 {
0645                     const QRgb col = m_introducedImage->pixel(x, y);
0646                     const int r = qRed(col);
0647                     if (r!=0 && r!=1 && r!=255) ++othercols;
0648                 }
0649             }
0650 
0651             m_isBW = (othercols==0);            // no other colours in image
0652         }
0653     }
0654 
0655     qDebug() << "cols" << cols << "format" << m_introducedImage->format() << "->" << m_isBW;
0656     m_resolvedBW = true;                // note result now resolved
0657     return (m_isBW);
0658 }