File indexing completed on 2024-04-28 15:39:39
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 "abstractocrdialogue.h" 0033 0034 #include <qlabel.h> 0035 #include <qgroupbox.h> 0036 #include <qcheckbox.h> 0037 #include <qlayout.h> 0038 #include <qprogressbar.h> 0039 #include <qapplication.h> 0040 #include <qradiobutton.h> 0041 #include <qpushbutton.h> 0042 #include <qicon.h> 0043 #include <qstandardpaths.h> 0044 0045 #include <klocalizedstring.h> 0046 #include <kstandardguiitem.h> 0047 #include <kseparator.h> 0048 0049 #include <kio/job.h> 0050 #include <kio/previewjob.h> 0051 0052 #include <sonnet/configdialog.h> 0053 0054 #include "kookasettings.h" 0055 0056 #include "imagecanvas.h" 0057 #include "dialogbase.h" 0058 #include "pluginmanager.h" 0059 #include "ocr_logging.h" 0060 0061 0062 AbstractOcrDialogue::AbstractOcrDialogue(AbstractOcrEngine *plugin, QWidget *pnt) 0063 : KPageDialog(pnt), 0064 m_plugin(plugin), 0065 m_setupPage(nullptr), 0066 m_sourcePage(nullptr), 0067 m_enginePage(nullptr), 0068 m_spellPage(nullptr), 0069 m_debugPage(nullptr), 0070 m_previewPix(nullptr), 0071 m_previewLabel(nullptr), 0072 m_wantDebugCfg(true), 0073 m_cbRetainFiles(nullptr), 0074 m_cbVerboseDebug(nullptr), 0075 m_retainFiles(false), 0076 m_verboseDebug(false), 0077 m_lVersion(nullptr), 0078 m_progress(nullptr) 0079 { 0080 setModal(true); 0081 0082 // The original buttons used in KDE4 were User1=Start, User2=Stop, Close. 0083 // Because the button actions must not simply accept or reject the dialogue 0084 // (closing it in both cases), we need to carefully choose the standard 0085 // buttons so that they do not perform those actions. This means that the 0086 // button cannot have an AcceptRole, RejectRole, YesRole or NoRole because 0087 // those all either accept or reject the dialogue. The dialogue needs to 0088 // stay open while OCR is in progress, because it shows the progress and 0089 // has the "Stop OCR" button. 0090 // 0091 // The buttons chosen also affect the placement, but the dialogue actions 0092 // are more important! 0093 // 0094 // So the buttons used with Qt5 are Discard=Start, Apply=Stop, Close. This 0095 // at least places the buttons in the intended order (in the standard KDE 0096 // style), even though the buttons used bear no relation to their function. 0097 0098 QDialogButtonBox *bb = buttonBox(); 0099 setStandardButtons(QDialogButtonBox::Discard|QDialogButtonBox::Apply|QDialogButtonBox::Close); 0100 bb->button(QDialogButtonBox::Discard)->setDefault(true); 0101 setWindowTitle(i18n("Optical Character Recognition")); 0102 0103 KGuiItem::assign(bb->button(QDialogButtonBox::Discard), KGuiItem(i18n("Start OCR"), "system-run", i18n("Start the Optical Character Recognition process"))); 0104 KGuiItem::assign(bb->button(QDialogButtonBox::Apply), KGuiItem(i18n("Stop OCR"), "process-stop", i18n("Stop the Optical Character Recognition process"))); 0105 0106 // Signals which tell our caller what the user is doing 0107 connect(bb->button(QDialogButtonBox::Discard), &QAbstractButton::clicked, this, &AbstractOcrDialogue::slotStartOCR); 0108 connect(bb->button(QDialogButtonBox::Apply), &QAbstractButton::clicked, this, &AbstractOcrDialogue::signalOcrStop); 0109 connect(this, &QDialog::rejected, this, &AbstractOcrDialogue::signalOcrClose); 0110 0111 m_previewSize.setWidth(380); // minimum preview size 0112 m_previewSize.setHeight(250); 0113 0114 bb->button(QDialogButtonBox::Discard)->setEnabled(true); // Start OCR 0115 bb->button(QDialogButtonBox::Apply)->setEnabled(false); // Stop OCR 0116 bb->button(QDialogButtonBox::Close)->setEnabled(true); // Close 0117 0118 // This appears to be necessary to ensure that "Start OCR" becomes the 0119 // default button. 0120 bb->button(QDialogButtonBox::Discard)->setFocus(Qt::OtherFocusReason); 0121 } 0122 0123 0124 bool AbstractOcrDialogue::setupGui() 0125 { 0126 setupSetupPage(); 0127 setupSpellPage(); 0128 setupSourcePage(); 0129 setupEnginePage(); 0130 // TODO: preferences option for whether debug is shown 0131 if (m_wantDebugCfg) setupDebugPage(); 0132 0133 return (true); 0134 } 0135 0136 0137 void AbstractOcrDialogue::setupSetupPage() 0138 { 0139 QWidget *w = new QWidget(this); 0140 QGridLayout *gl = new QGridLayout(w); 0141 Q_UNUSED(gl); // retrieved via layout() 0142 0143 m_progress = new QProgressBar(this); 0144 m_progress->setVisible(false); 0145 0146 m_setupPage = addPage(w, i18n("Setup")); 0147 0148 const AbstractPluginInfo *info = engine()->pluginInfo(); 0149 m_setupPage->setHeader(i18n("Optical Character Recognition using %1", info->name)); 0150 m_setupPage->setIcon(QIcon::fromTheme("ocr")); 0151 } 0152 0153 0154 QWidget *AbstractOcrDialogue::addExtraPageWidget(KPageWidgetItem *page, QWidget *wid, bool stretchBefore) 0155 { 0156 QGridLayout *gl = static_cast<QGridLayout *>(page->widget()->layout()); 0157 int nextrow = gl->rowCount(); 0158 // rowCount() seems to return 1 even if the layout is empty... 0159 if (gl->itemAtPosition(0, 0) == nullptr) { 0160 nextrow = 0; 0161 } 0162 0163 if (stretchBefore) { // stretch before new row 0164 gl->setRowStretch(nextrow, 1); 0165 ++nextrow; 0166 } else if (nextrow > 0) { // something there already, 0167 // add separator line 0168 gl->addWidget(new KSeparator(Qt::Horizontal, this), nextrow, 0, 1, 2); 0169 ++nextrow; 0170 } 0171 0172 if (wid == nullptr) { 0173 wid = new QWidget(this); 0174 } 0175 gl->addWidget(wid, nextrow, 0, 1, 2); 0176 0177 return (wid); 0178 } 0179 0180 0181 QWidget *AbstractOcrDialogue::addExtraSetupWidget(QWidget *wid, bool stretchBefore) 0182 { 0183 return (addExtraPageWidget(m_setupPage, wid, stretchBefore)); 0184 } 0185 0186 0187 void AbstractOcrDialogue::ocrShowInfo(const QString &binary, const QString &version) 0188 { 0189 QWidget *w = addExtraEngineWidget(); // engine path/version/icon 0190 QGridLayout *gl = new QGridLayout(w); 0191 0192 QLabel *l = new QLabel(i18n("Executable:"), w); 0193 gl->addWidget(l, 0, 0, Qt::AlignLeft | Qt::AlignTop); 0194 0195 l = new QLabel((!binary.isEmpty() ? xi18nc("@info", "<filename>%1</filename>", binary) : i18n("Not found")), w); 0196 gl->addWidget(l, 0, 1, Qt::AlignLeft | Qt::AlignTop); 0197 0198 l = new QLabel(i18n("Version:"), w); 0199 gl->addWidget(l, 1, 0, Qt::AlignLeft | Qt::AlignTop); 0200 0201 m_lVersion = new QLabel((!version.isEmpty() ? version : i18n("Unknown")), w); 0202 gl->addWidget(m_lVersion, 1, 1, Qt::AlignLeft | Qt::AlignTop); 0203 0204 // Find the logo and display it if available 0205 const AbstractPluginInfo *info = engine()->pluginInfo(); 0206 QString logoFile = KIconLoader::global()->iconPath(info->icon, KIconLoader::NoGroup, true); 0207 if (!logoFile.isNull()) 0208 { 0209 QLabel *l = new QLabel(w); 0210 l->setPixmap(QPixmap(logoFile)); 0211 gl->addWidget(l, 0, 3, 3, 1, Qt::AlignRight); 0212 } 0213 0214 gl->setColumnStretch(2, 1); 0215 } 0216 0217 0218 void AbstractOcrDialogue::ocrShowVersion(const QString &version) 0219 { 0220 if (m_lVersion != nullptr) { 0221 m_lVersion->setText(version); 0222 } 0223 } 0224 0225 0226 void AbstractOcrDialogue::setupSourcePage() 0227 { 0228 QWidget *w = new QWidget(this); 0229 QGridLayout *gl = new QGridLayout(w); 0230 0231 // These labels are filled with the preview pixmap and image 0232 // information in introduceImage() 0233 m_previewPix = new QLabel(i18n("No preview available"), w); 0234 m_previewPix->setPixmap(QPixmap()); 0235 m_previewPix->setMinimumSize(m_previewSize.width() + 2*DialogBase::horizontalSpacing(), 0236 m_previewSize.height() + 2*DialogBase::verticalSpacing()); 0237 m_previewPix->setAlignment(Qt::AlignCenter); 0238 m_previewPix->setFrameStyle(QFrame::Panel | QFrame::Sunken); 0239 gl->addWidget(m_previewPix, 0, 0); 0240 gl->setRowStretch(0, 1); 0241 0242 m_previewLabel = new QLabel(i18n("No information available"), w); 0243 gl->addWidget(m_previewLabel, 1, 0, Qt::AlignHCenter); 0244 0245 m_sourcePage = addPage(w, i18n("Source")); 0246 m_sourcePage->setHeader(i18n("Source Image Information")); 0247 m_sourcePage->setIcon(QIcon::fromTheme("dialog-information")); 0248 } 0249 0250 0251 void AbstractOcrDialogue::setupEnginePage() 0252 { 0253 QWidget *w = new QWidget(this); // engine title/logo/description 0254 QGridLayout *gl = new QGridLayout(w); 0255 0256 const AbstractPluginInfo *info = engine()->pluginInfo(); 0257 QLabel *l = new QLabel(info->description, w); 0258 l->setWordWrap(true); 0259 l->setOpenExternalLinks(true); 0260 gl->addWidget(l, 0, 0, 1, 2, Qt::AlignTop); 0261 0262 gl->setRowStretch(2, 1); 0263 gl->setColumnStretch(0, 1); 0264 0265 m_enginePage = addPage(w, i18n("OCR Engine")); 0266 m_enginePage->setHeader(i18n("OCR Engine Information")); 0267 m_enginePage->setIcon(QIcon::fromTheme("application-x-executable")); 0268 } 0269 0270 0271 QWidget *AbstractOcrDialogue::addExtraEngineWidget(QWidget *wid, bool stretchBefore) 0272 { 0273 return (addExtraPageWidget(m_enginePage, wid, stretchBefore)); 0274 } 0275 0276 0277 void AbstractOcrDialogue::setupSpellPage() 0278 { 0279 QWidget *w = new QWidget(this); 0280 QGridLayout *gl = new QGridLayout(w); 0281 0282 // row 0: background checking group box 0283 m_gbBackgroundCheck = new QGroupBox(i18n("Highlight misspelled words"), w); 0284 m_gbBackgroundCheck->setCheckable(true); 0285 0286 QGridLayout *gl1 = new QGridLayout(m_gbBackgroundCheck); 0287 m_gbBackgroundCheck->setLayout(gl1); 0288 0289 m_rbGlobalSpellSettings = new QRadioButton(i18n("Use the system spell configuration"), w); 0290 gl1->addWidget(m_rbGlobalSpellSettings, 0, 0); 0291 m_rbCustomSpellSettings = new QRadioButton(i18n("Use custom spell configuration"), w); 0292 gl1->addWidget(m_rbCustomSpellSettings, 1, 0); 0293 m_pbCustomSpellDialog = new QPushButton(i18n("Custom Spell Configuration..."), w); 0294 gl1->addWidget(m_pbCustomSpellDialog, 2, 0, Qt::AlignRight); 0295 connect(m_rbCustomSpellSettings, &QAbstractButton::toggled, m_pbCustomSpellDialog, &QWidget::setEnabled); 0296 connect(m_pbCustomSpellDialog, &QAbstractButton::clicked, this, &AbstractOcrDialogue::slotCustomSpellDialog); 0297 gl->addWidget(m_gbBackgroundCheck, 0, 0); 0298 0299 // row 1: space 0300 gl->setRowMinimumHeight(1, 2*DialogBase::verticalSpacing()); 0301 0302 // row 2: interactive checking group box 0303 m_gbInteractiveCheck = new QGroupBox(i18n("Start interactive spell check"), w); 0304 m_gbInteractiveCheck->setCheckable(true); 0305 0306 QGridLayout *gl2 = new QGridLayout(m_gbInteractiveCheck); 0307 m_gbInteractiveCheck->setLayout(gl2); 0308 0309 QLabel *l = new QLabel(i18n("Custom spell settings above do not affect this spelling check, use the language setting in the dialog to change the dictionary language."), w); 0310 l->setWordWrap(true); 0311 gl2->addWidget(l, 0, 0); 0312 0313 gl->addWidget(m_gbInteractiveCheck, 2, 0); 0314 0315 // row 3: stretch 0316 gl->setRowStretch(3, 1); 0317 0318 // Apply settings 0319 m_gbBackgroundCheck->setChecked(KookaSettings::ocrSpellBackgroundCheck()); 0320 m_gbInteractiveCheck->setChecked(KookaSettings::ocrSpellInteractiveCheck()); 0321 0322 #ifndef KF5 0323 const bool customSettings = KookaSettings::ocrSpellCustomSettings(); 0324 #else 0325 const bool customSettings = false; 0326 m_rbCustomSpellSettings->setEnabled(false); 0327 #endif 0328 m_rbGlobalSpellSettings->setChecked(!customSettings); 0329 m_rbCustomSpellSettings->setChecked(customSettings); 0330 m_pbCustomSpellDialog->setEnabled(customSettings); 0331 0332 m_spellPage = addPage(w, i18n("Spell Check")); 0333 m_spellPage->setHeader(i18n("OCR Result Spell Checking")); 0334 m_spellPage->setIcon(QIcon::fromTheme("tools-check-spelling")); 0335 } 0336 0337 0338 void AbstractOcrDialogue::setupDebugPage() 0339 { 0340 QWidget *w = new QWidget(this); 0341 QGridLayout *gl = new QGridLayout(w); 0342 0343 m_cbRetainFiles = new QCheckBox(i18n("Retain temporary files"), w); 0344 gl->addWidget(m_cbRetainFiles, 0, 0, Qt::AlignTop); 0345 0346 m_cbVerboseDebug = new QCheckBox(i18n("Verbose message output"), w); 0347 gl->addWidget(m_cbVerboseDebug, 1, 0, Qt::AlignTop); 0348 0349 gl->setRowStretch(2, 1); 0350 0351 m_debugPage = addPage(w, i18n("Debugging")); 0352 m_debugPage->setHeader(i18n("OCR Debugging")); 0353 m_debugPage->setIcon(QIcon::fromTheme("tools-report-bug")); 0354 } 0355 0356 0357 void AbstractOcrDialogue::stopAnimation() 0358 { 0359 if (m_progress != nullptr) { 0360 m_progress->setVisible(false); 0361 } 0362 } 0363 0364 0365 void AbstractOcrDialogue::startAnimation() 0366 { 0367 // If the progress bar range has been set to (0,0) then the engine will 0368 // not be providing a detailed progress percentage, only a busy indication. 0369 // In this case, set the value to 1 now to start the animation. 0370 // If there is a maximum then there will be a detailed progress 0371 // percentage, so set the initial value to the minimum. 0372 m_progress->setValue(m_progress->maximum()==0 ? 1 : m_progress->minimum()); 0373 0374 if (!m_progress->isVisible()) { // progress bar not added yet 0375 addExtraSetupWidget(m_progress, true); 0376 m_progress->setVisible(true); 0377 } 0378 } 0379 0380 // Not sure why this uses an asynchronous preview job for the image thumbnail 0381 // (if it is possible, i.e. the image is file bound) as opposed to just scaling 0382 // the image (which is always loaded at this point, i.e. it is already in memory). 0383 // Possibly because scaling a potentially very large image could introduce a 0384 // significant delay in opening the dialogue box, so making the GUI appear 0385 // less responsive. So we'll keep the preview job for now. 0386 // 0387 // We now bring you a mild rant... 0388 // 0389 // What on earth happened to KFileMetaInfo in KDE4? This used to have a fairly 0390 // reasonable API, returning a list of key-value pairs grouped into sensible 0391 // categories with readable strings available for each. Now the groups have 0392 // gone (so for example methods such as preferredGroups(), albeit being marked as 0393 // 'deprecated', return an empty list!) and the key of each entry is an ontology 0394 // URL. Not sure what to do with this URL (although I'm sure it must be of 0395 // interest to something), and it doesn't even return the minimal useful 0396 // information (e.g. the size/depth) for many image file types anyway. 0397 // 0398 // Could this be why the "Meta Info" tab of the file properties dialogue also 0399 // seems to have disappeared? 0400 // 0401 // So forget about KFileMetaInfo here, just display a simple label with the 0402 // image size and depth (which information we already have available). 0403 0404 void AbstractOcrDialogue::introduceImage(ScanImage::Ptr img) 0405 { 0406 if (img.isNull()) 0407 { 0408 if (m_previewLabel!=nullptr) m_previewLabel->setText(i18n("No image")); 0409 return; 0410 } 0411 0412 qCDebug(OCR_LOG) << "url" << img->url() << "filebound" << img->isFileBound(); 0413 0414 if (img->isFileBound()) { // image backed by a file 0415 /* Start to create a preview job for the thumb */ 0416 KFileItemList fileItems; 0417 fileItems.append(KFileItem(img->url())); 0418 0419 KIO::PreviewJob *job = KIO::filePreview(fileItems, QSize(m_previewSize.width(), m_previewSize.height())); 0420 if (job!=nullptr) { 0421 job->setIgnoreMaximumSize(); 0422 connect(job, &KIO::PreviewJob::gotPreview, this, &AbstractOcrDialogue::slotGotPreview); 0423 } 0424 } else { // selection only in memory, 0425 // do the preview ourselves 0426 QImage qimg = img->scaled(m_previewSize, Qt::KeepAspectRatio, Qt::SmoothTransformation); 0427 slotGotPreview(KFileItem(), QPixmap::fromImage(qimg)); 0428 } 0429 0430 if (m_previewLabel != nullptr) { 0431 KLocalizedString str = img->isFileBound() ? ki18n("Image: %1") : ki18n("Selection: %1"); 0432 m_previewLabel->setText(str.subs(ImageCanvas::imageInfoString(img.data())).toString()); 0433 } 0434 } 0435 0436 0437 bool AbstractOcrDialogue::keepTempFiles() const 0438 { 0439 return (m_retainFiles); 0440 } 0441 0442 0443 bool AbstractOcrDialogue::verboseDebug() const 0444 { 0445 return (m_verboseDebug); 0446 } 0447 0448 0449 void AbstractOcrDialogue::slotGotPreview(const KFileItem &item, const QPixmap &newPix) 0450 { 0451 qCDebug(OCR_LOG) << "pixmap" << newPix.size(); 0452 if (m_previewPix != nullptr) { 0453 m_previewPix->setText(QString()); 0454 m_previewPix->setPixmap(newPix); 0455 } 0456 } 0457 0458 0459 void AbstractOcrDialogue::slotWriteConfig() 0460 { 0461 KookaSettings::setOcrSpellBackgroundCheck(m_gbBackgroundCheck->isChecked()); 0462 KookaSettings::setOcrSpellInteractiveCheck(m_gbInteractiveCheck->isChecked()); 0463 KookaSettings::setOcrSpellCustomSettings(m_rbCustomSpellSettings->isChecked()); 0464 KookaSettings::self()->save(); 0465 // deliberately not saving the OCR debug configuration 0466 } 0467 0468 0469 void AbstractOcrDialogue::slotStartOCR() 0470 { 0471 setCurrentPage(m_setupPage); // force back to first page 0472 0473 m_retainFiles = (m_cbRetainFiles != nullptr && m_cbRetainFiles->isChecked()); 0474 m_verboseDebug = (m_cbVerboseDebug != nullptr && m_cbVerboseDebug->isChecked()); 0475 0476 slotWriteConfig(); // save configuration 0477 emit signalOcrStart(); // start the OCR process 0478 } 0479 0480 0481 void AbstractOcrDialogue::enableGUI(bool running) 0482 { 0483 m_sourcePage->setEnabled(!running); 0484 m_enginePage->setEnabled(!running); 0485 if (m_spellPage != nullptr) m_spellPage->setEnabled(!running); 0486 if (m_debugPage != nullptr) m_debugPage->setEnabled(!running); 0487 enableFields(!running); // engine's GUI widgets 0488 0489 if (running) startAnimation(); // start our progress bar 0490 else stopAnimation(); // stop our progress bar 0491 0492 QDialogButtonBox *bb = buttonBox(); 0493 bb->button(QDialogButtonBox::Discard)->setEnabled(!running); // Start OCR 0494 bb->button(QDialogButtonBox::Apply)->setEnabled(running); // Stop OCR 0495 bb->button(QDialogButtonBox::Close)->setEnabled(!running); // Close 0496 0497 QApplication::processEvents(); // ensure GUI up-to-date 0498 } 0499 0500 0501 bool AbstractOcrDialogue::wantInteractiveSpellCheck() const 0502 { 0503 return (m_gbInteractiveCheck->isChecked()); 0504 } 0505 0506 0507 bool AbstractOcrDialogue::wantBackgroundSpellCheck() const 0508 { 0509 return (m_gbBackgroundCheck->isChecked()); 0510 } 0511 0512 0513 QString AbstractOcrDialogue::customSpellConfigFile() const 0514 { 0515 if (m_rbCustomSpellSettings->isChecked()) { // our application config 0516 return (KSharedConfig::openConfig()->name()); 0517 } 0518 return ("sonnetrc"); // Sonnet global settings 0519 } 0520 0521 0522 QProgressBar *AbstractOcrDialogue::progressBar() const 0523 { 0524 return (m_progress); 0525 } 0526 0527 0528 void AbstractOcrDialogue::slotCustomSpellDialog() 0529 { 0530 #ifndef KF5 0531 // TODO: Sonnet in KF5 appears to no longer allow a custom configuration, 0532 // QSettings("KDE","Sonnet") is hardwired in Settings::restore() in 0533 // sonnet/src/core/settings.cpp 0534 // See also KookaView::slotSetOcrSpellConfig() 0535 // It may be possible, though, to configure only the language; 0536 // see http://api.kde.org/frameworks-api/frameworks5-apidocs/sonnet/html/classSonnet_1_1ConfigDialog.html 0537 Sonnet::ConfigDialog d(this); 0538 // Sonnet::ConfigDialog d(KSharedConfig::openConfig().data(), this); 0539 d.exec(); // save to our application config 0540 #endif 0541 }