diff --git a/CMakeLists.txt b/CMakeLists.txt index 6d89b0c..27ba8eb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,131 +1,135 @@ ########################################################################## ## ## ## This CMake file is part of Kooka, a KDE scanning/OCR application. ## ## ## ## This file may be distributed and/or modified under the terms of ## ## the GNU General Public License version 2, as published by the ## ## Free Software Foundation and appearing in the file COPYING ## ## included in the packaging of this file. ## ## ## ## Author: Jonathan Marten ## ## ## ########################################################################## cmake_minimum_required(VERSION 2.8.12) project(kooka5) set(VERSION "0.90") message(STATUS "Configuring for Kooka/libkookascan version ${VERSION}") cmake_minimum_required (VERSION 2.8.12 FATAL_ERROR) set(QT_MIN_VERSION "5.4.0") set(KF5_MIN_VERSION "5.10.0") set(ECM_MIN_VERSION "1.2.0") # ECM setup (Extra Cmake Modules) find_package(ECM ${ECM_MIN_VERSION} REQUIRED NO_MODULE) set(CMAKE_MODULE_PATH ${ECM_MODULE_PATH} ${ECM_KDE_MODULE_DIR}) include(FeatureSummary) include(ECMSetupVersion) include(ECMGenerateHeaders) include(ECMPackageConfigHelpers) include(CheckFunctionExists) include(KDEInstallDirs) include(KDEFrameworkCompilerSettings NO_POLICY_SCOPE) include(KDECMakeSettings) include(GenerateExportHeader) include(ECMInstallIcons) # Options option(INSTALL_BINARIES "Install the binaries and libraries, turn off for development in place" ON) # Required Qt5 components to build this package find_package(Qt5 ${QT_MIN_VERSION} REQUIRED COMPONENTS Core Widgets) # Rigourousness add_definitions("-DQT_USE_FAST_CONCATENATION") add_definitions("-DQT_USE_FAST_OPERATOR_PLUS") add_definitions("-DQT_NO_CAST_FROM_BYTEARRAY") add_definitions("-DQT_NO_NARROWING_CONVERSIONS_IN_CONNECT") add_definitions("-DQT_NO_CAST_TO_ASCII") add_definitions("-DQT_NO_URL_CAST_FROM_STRING") # Permissiveness remove_definitions("-DQT_NO_CAST_FROM_ASCII") remove_definitions("-DQT_NO_SIGNALS_SLOTS_KEYWORDS") # Support for SANE, here because library and sanedump both need it # # Prefer pkg-config(1), because sane-config(1) was removed from Debian # package sane-backends 1.0.25 in December 2016. Assuming here that # pkg-config(1) is available on any reasonable system. find_package(PkgConfig) if (PkgConfig_FOUND) pkg_check_modules(SANE sane-backends) endif (PkgConfig_FOUND) if (SANE_FOUND) set(SANE_INCLUDES "${SANE_CFLAGS}") set(SANE_LIBRARIES "${SANE_LDFLAGS}") else (SANE_FOUND) # if pkg-config(1) did not find anything, then fall back to sane-config(1) message(STATUS "SANE not found via pkg-config(1), trying sane-config(1)") if (SANECONFIG_BIN) set(SANECONFIG_PROG ${SANECONFIG_BIN}) message(STATUS "Specified sane-config(1), ${SANECONFIG_PROG}") else (SANECONFIG_BIN) find_program(SANECONFIG_PROG NAMES sane-config) message(STATUS "Found sane-config(1), ${SANECONFIG_PROG}") endif (SANECONFIG_BIN) if (SANECONFIG_PROG) set(SANE_FOUND true) execute_process(COMMAND ${SANECONFIG_PROG} --version OUTPUT_VARIABLE SANE_VERSION OUTPUT_STRIP_TRAILING_WHITESPACE) execute_process(COMMAND ${SANECONFIG_PROG} --cflags OUTPUT_VARIABLE SANE_INCLUDES OUTPUT_STRIP_TRAILING_WHITESPACE) execute_process(COMMAND ${SANECONFIG_PROG} --libs OUTPUT_VARIABLE SANE_LIBRARIES OUTPUT_STRIP_TRAILING_WHITESPACE) message(STATUS "Found SANE, version ${SANE_VERSION}") endif (SANECONFIG_PROG) endif (SANE_FOUND) if (SANE_FOUND) set(HAVE_SANE true) message(STATUS " SANE includes: ${SANE_INCLUDES}") message(STATUS " SANE libraries: ${SANE_LIBRARIES}") else (SANE_FOUND) message(SEND_ERROR "libkookascan needs SANE (http://www.sane-project.org) - install package or specify location of sane-config(1) with SANECONFIG_BIN") endif (SANE_FOUND) +############### Common install locations ############### + +set(PICS_INSTALL_DIR ${DATA_INSTALL_DIR}/kooka/pics) + ############### Now, we add the Kooka components ############### add_subdirectory(libdialogutil) add_subdirectory(libkookascan) add_subdirectory(libfiletree) add_subdirectory(app) add_subdirectory(plugins) add_subdirectory(doc) add_subdirectory(tools EXCLUDE_FROM_ALL) ############### VCS revision number in vcsversion.h ############### add_custom_target(vcsversion ALL COMMENT "Checking VCS version" VERBATIM COMMAND sh ${CMAKE_CURRENT_SOURCE_DIR}/vcsversion.sh ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_BINARY_DIR} ${VERSION} ) # ########### documentation ############### # # if (HAVE_APIDOX) # add_custom_target(apidox # COMMENT "Generating API documentation in ${CMAKE_CURRENT_BINARY_DIR}..." # VERBATIM # COMMAND sh -c "${KDELIBS_SOURCE_DIR}/doc/api/doxygen.sh --no-modulename --recurse --doxdatadir=${KDELIBS_SOURCE_DIR}/doc/common ${CMAKE_CURRENT_SOURCE_DIR}; echo 'API documentation at file://${CMAKE_CURRENT_BINARY_DIR}/apidocs/index.html';") # endif (HAVE_APIDOX) ############### Configuration information ############### feature_summary(WHAT ALL FATAL_ON_MISSING_REQUIRED_PACKAGES) diff --git a/app/kookasettings.kcfg b/app/kookasettings.kcfg index da77fbd..73b9d35 100644 --- a/app/kookasettings.kcfg +++ b/app/kookasettings.kcfg @@ -1,309 +1,346 @@ kookaprint.h kiconloader.h 0 0 <div>Check this if you want Kooka to load the last selected image into the viewer on startup.<br/><br/>If your images are large, that might slow down Kooka's startup.</div> false <div>Check this if you want to always use the image save assistant, even if there is a default format for the image type.</div> false <div>Check this if you want to enter a file name when scanning an image.</div> false <div>Check this if you want to enter the file name before scanning starts.</div> true <div>Check this if you want to enter the file name after scanning has finished.</div> false <div>Check this to only show recommended file formats for the scan type. Uncheck it to show all compatible formats.</div> false <div>Check this to remember the selected file format for the scanned image type and not ask for it again.</div> (20*1024*1024LL) <div>Check this if you want to be able to rename gallery items by clicking on them (otherwise, use the "Rename" menu option).</div> false <div>Select whether and where a list of recently accessed gallery folders will be shown.</div> KookaSettings::RecentAtTop KookaGallery <div>Set a custom background image for the thumbnail view.</div> false <div>The background image to use for the thumbnail view.</div> <div>Select the default preview size for the thumbnail view.</div> KIconLoader::SizeHuge true false false gocr <div>Clusters smaller than this size will be considered to be noise, and ignored. The default is 10.</div> 10 <div>The threshold value below which gray pixels are considered to be black. The default is 160.</div> 160 <div>Spacing between characters. The default is 0 which means autodetection.</div> 0 ocrad 0 utf8 false false 50 + + + + tesseract + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + KookaPrint::ScaleScan QSize(100,100) true false KookaPrint::CutMarksMultiple /tmp/print.pdf diff --git a/app/pics/CMakeLists.txt b/app/pics/CMakeLists.txt index 5db336e..9f19f38 100644 --- a/app/pics/CMakeLists.txt +++ b/app/pics/CMakeLists.txt @@ -1,43 +1,41 @@ ########################################################################## ## ## ## This CMake file is part of Kooka, a KDE scanning/OCR application. ## ## ## ## This file may be distributed and/or modified under the terms of ## ## the GNU General Public License version 2, as published by the ## ## Free Software Foundation and appearing in the file COPYING ## ## included in the packaging of this file. ## ## ## ## Author: Jonathan Marten ## ## ## ########################################################################## project(kooka5) ########### install files ############### set(kooka_PICS lockzoom.png mirror-both.png mirror-horiz.png mirror-vert.png newfromselect.png ocr.png ocr-select.png - gocr.png - ocrad.png photocopy.png preview.png scan.png scanadd.png scanselect.png scaleorig.png scaletoheight.png scaletowidth.png thumbviewtile.png rotate-acw.png rotate-cw.png rotate-180.png autoselect.png ) -install(FILES ${kooka_PICS} DESTINATION ${DATA_INSTALL_DIR}/kooka/pics) +install(FILES ${kooka_PICS} DESTINATION ${PICS_INSTALL_DIR}) diff --git a/plugins/ocr/CMakeLists.txt b/plugins/ocr/CMakeLists.txt index ea19df1..d554cdd 100644 --- a/plugins/ocr/CMakeLists.txt +++ b/plugins/ocr/CMakeLists.txt @@ -1,65 +1,66 @@ ########################################################################## ## ## ## This CMake file is part of Kooka, a KDE scanning/OCR application. ## ## ## ## This file may be distributed and/or modified under the terms of ## ## the GNU General Public License version 2, as published by the ## ## Free Software Foundation and appearing in the file COPYING ## ## included in the packaging of this file. ## ## ## ## Author: Jonathan Marten ## ## ## ########################################################################## project(kooka5) ######################################################################### # # # Options # # # ######################################################################### option(WITH_KADMOS "Enable the Kadmos OCR engine" false) ######################################################################### # # # Additional dependencies for OCR # # # ######################################################################### find_package(KF5 ${KF5_MIN_VERSION} REQUIRED COMPONENTS I18n ConfigWidgets TextWidgets IconThemes Sonnet WidgetsAddons) ######################################################################### # # # OCR plugin base library # # # ######################################################################### set(kookaocr_SRCS abstractocrengine.cpp abstractocrdialogue.cpp executablepathdialogue.cpp ) add_library(kookaocr SHARED ${kookaocr_SRCS}) set_target_properties(kookaocr PROPERTIES SOVERSION "1.0.0") target_link_libraries(kookaocr Qt5::Core Qt5::Widgets) target_link_libraries(kookaocr KF5::I18n KF5::WidgetsAddons KF5::ConfigWidgets KF5::SonnetCore KF5::SonnetUi) target_link_libraries(kookaocr kookascan kookacore) target_link_libraries(kookaocr dialogutil) if (INSTALL_BINARIES) install(TARGETS kookaocr ${INSTALL_TARGETS_DEFAULT_ARGS}) endif (INSTALL_BINARIES) install(FILES kookaocrplugin.desktop DESTINATION ${SERVICETYPES_INSTALL_DIR}) ######################################################################### # # # Subdirectories # # # ######################################################################### add_subdirectory(gocr) add_subdirectory(ocrad) +add_subdirectory(tesseract) if(WITH_KADMOS) add_subdirectory(kadmos) endif(WITH_KADMOS) diff --git a/plugins/ocr/abstractocrdialogue.cpp b/plugins/ocr/abstractocrdialogue.cpp index e863676..05f9d91 100644 --- a/plugins/ocr/abstractocrdialogue.cpp +++ b/plugins/ocr/abstractocrdialogue.cpp @@ -1,536 +1,536 @@ /************************************************************************ * * * This file is part of Kooka, a scanning/OCR application using * * Qt and KDE Frameworks . * * * * Copyright (C) 2000-2016 Klaas Freitag * * Jonathan Marten * * * * Kooka is free software; you can redistribute it and/or modify it * * under the terms of the GNU Library General Public License as * * published by the Free Software Foundation and appearing in the * * file COPYING included in the packaging of this file; either * * version 2 of the License, or (at your option) any later version. * * * * As a special exception, permission is given to link this program * * with any version of the KADMOS OCR/ICR engine (a product of * * reRecognition GmbH, Kreuzlingen), and distribute the resulting * * executable without including the source code for KADMOS in the * * source distribution. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public * * License along with this program; see the file COPYING. If * * not, see . * * * ************************************************************************/ #include "abstractocrdialogue.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "kookaimage.h" #include "kookasettings.h" #include "imagecanvas.h" #include "dialogbase.h" #include "pluginmanager.h" AbstractOcrDialogue::AbstractOcrDialogue(AbstractOcrEngine *plugin, QWidget *pnt) : KPageDialog(pnt), m_plugin(plugin), m_setupPage(nullptr), m_sourcePage(nullptr), m_enginePage(nullptr), m_spellPage(nullptr), m_debugPage(nullptr), m_previewPix(nullptr), m_previewLabel(nullptr), m_wantDebugCfg(true), m_cbRetainFiles(nullptr), m_cbVerboseDebug(nullptr), m_retainFiles(false), m_verboseDebug(false), m_lVersion(nullptr), m_progress(nullptr) { setModal(true); // The original buttons used in KDE4 were User1=Start, User2=Stop, Close. // Because the button actions must not simply accept or reject the dialogue // (closing it in both cases), we need to carefully choose the standard // buttons so that they do not perform those actions. This means that the // button cannot have an AcceptRole, RejectRole, YesRole or NoRole because // those all either accept or reject the dialogue. The dialogue needs to // stay open while OCR is in progress, because it shows the progress and // has the "Stop OCR" button. // // The buttons chosen also affect the placement, but the dialogue actions // are more important! // // So the buttons used in Qt5 are Discard=Start, Apply=Stop, Close. This at // at least places the buttons in the intended order (in the standard KDE // style), even though the buttons used bear no relation to their function. QDialogButtonBox *bb = buttonBox(); setStandardButtons(QDialogButtonBox::Discard|QDialogButtonBox::Apply|QDialogButtonBox::Close); bb->button(QDialogButtonBox::Discard)->setDefault(true); setWindowTitle(i18n("Optical Character Recognition")); KGuiItem::assign(bb->button(QDialogButtonBox::Discard), KGuiItem(i18n("Start OCR"), "system-run", i18n("Start the Optical Character Recognition process"))); KGuiItem::assign(bb->button(QDialogButtonBox::Apply), KGuiItem(i18n("Stop OCR"), "process-stop", i18n("Stop the Optical Character Recognition process"))); // Signals which tell our caller what the user is doing connect(bb->button(QDialogButtonBox::Discard), &QAbstractButton::clicked, this, &AbstractOcrDialogue::slotStartOCR); connect(bb->button(QDialogButtonBox::Apply), &QAbstractButton::clicked, this, &AbstractOcrDialogue::signalOcrStop); connect(this, &QDialog::rejected, this, &AbstractOcrDialogue::signalOcrClose); m_previewSize.setWidth(380); // minimum preview size m_previewSize.setHeight(250); bb->button(QDialogButtonBox::Discard)->setEnabled(true); // Start OCR bb->button(QDialogButtonBox::Apply)->setEnabled(false); // Stop OCR bb->button(QDialogButtonBox::Close)->setEnabled(true); // Close } bool AbstractOcrDialogue::setupGui() { setupSetupPage(); setupSpellPage(); setupSourcePage(); setupEnginePage(); // TODO: preferences option for whether debug is shown if (m_wantDebugCfg) setupDebugPage(); return (true); } void AbstractOcrDialogue::setupSetupPage() { QWidget *w = new QWidget(this); QGridLayout *gl = new QGridLayout(w); Q_UNUSED(gl); // retrieved via layout() m_progress = new QProgressBar(this); m_progress->setVisible(false); m_setupPage = addPage(w, i18n("Setup")); const AbstractPluginInfo *info = engine()->pluginInfo(); m_setupPage->setHeader(i18n("Optical Character Recognition using %1", info->name)); m_setupPage->setIcon(QIcon::fromTheme("ocr")); } QWidget *AbstractOcrDialogue::addExtraPageWidget(KPageWidgetItem *page, QWidget *wid, bool stretchBefore) { QGridLayout *gl = static_cast(page->widget()->layout()); int nextrow = gl->rowCount(); // rowCount() seems to return 1 even if the layout is empty... if (gl->itemAtPosition(0, 0) == nullptr) { nextrow = 0; } if (stretchBefore) { // stretch before new row gl->setRowStretch(nextrow, 1); ++nextrow; } else if (nextrow > 0) { // something there already, // add separator line gl->addWidget(new KSeparator(Qt::Horizontal, this), nextrow, 0, 1, 2); ++nextrow; } if (wid == nullptr) { wid = new QWidget(this); } gl->addWidget(wid, nextrow, 0, 1, 2); return (wid); } QWidget *AbstractOcrDialogue::addExtraSetupWidget(QWidget *wid, bool stretchBefore) { return (addExtraPageWidget(m_setupPage, wid, stretchBefore)); } void AbstractOcrDialogue::ocrShowInfo(const QString &binary, const QString &version) { QWidget *w = addExtraEngineWidget(); // engine path/version/icon QGridLayout *gl = new QGridLayout(w); QLabel *l = new QLabel(i18n("Executable:"), w); gl->addWidget(l, 0, 0, Qt::AlignLeft | Qt::AlignTop); l = new QLabel((!binary.isEmpty() ? xi18nc("@info", "%1", binary) : i18n("Not found")), w); gl->addWidget(l, 0, 1, Qt::AlignLeft | Qt::AlignTop); l = new QLabel(i18n("Version:"), w); gl->addWidget(l, 1, 0, Qt::AlignLeft | Qt::AlignTop); m_lVersion = new QLabel((!version.isEmpty() ? version : i18n("Unknown")), w); gl->addWidget(m_lVersion, 1, 1, Qt::AlignLeft | Qt::AlignTop); // Find the logo and display it if available const AbstractPluginInfo *info = engine()->pluginInfo(); QString logoFile = KIconLoader::global()->iconPath(info->icon, KIconLoader::NoGroup, true); if (!logoFile.isNull()) { QLabel *l = new QLabel(w); l->setPixmap(QPixmap(logoFile)); - gl->addWidget(l, 0, 3, 2, 1, Qt::AlignRight); + gl->addWidget(l, 0, 3, 3, 1, Qt::AlignRight); } gl->setColumnStretch(2, 1); } void AbstractOcrDialogue::ocrShowVersion(const QString &version) { if (m_lVersion != nullptr) { m_lVersion->setText(version); } } void AbstractOcrDialogue::setupSourcePage() { QWidget *w = new QWidget(this); QGridLayout *gl = new QGridLayout(w); // These labels are filled with the preview pixmap and image // information in introduceImage() m_previewPix = new QLabel(i18n("No preview available"), w); m_previewPix->setPixmap(QPixmap()); m_previewPix->setMinimumSize(m_previewSize.width() + 2*DialogBase::horizontalSpacing(), m_previewSize.height() + 2*DialogBase::verticalSpacing()); m_previewPix->setAlignment(Qt::AlignCenter); m_previewPix->setFrameStyle(QFrame::Panel | QFrame::Sunken); gl->addWidget(m_previewPix, 0, 0); gl->setRowStretch(0, 1); m_previewLabel = new QLabel(i18n("No information available"), w); gl->addWidget(m_previewLabel, 1, 0, Qt::AlignHCenter); m_sourcePage = addPage(w, i18n("Source")); m_sourcePage->setHeader(i18n("Source Image Information")); m_sourcePage->setIcon(QIcon::fromTheme("dialog-information")); } void AbstractOcrDialogue::setupEnginePage() { QWidget *w = new QWidget(this); // engine title/logo/description QGridLayout *gl = new QGridLayout(w); const AbstractPluginInfo *info = engine()->pluginInfo(); QLabel *l = new QLabel(info->description, w); l->setWordWrap(true); l->setOpenExternalLinks(true); gl->addWidget(l, 0, 0, 1, 2, Qt::AlignTop); - gl->setRowStretch(0, 1); + gl->setRowStretch(2, 1); gl->setColumnStretch(0, 1); m_enginePage = addPage(w, i18n("OCR Engine")); m_enginePage->setHeader(i18n("OCR Engine Information")); m_enginePage->setIcon(QIcon::fromTheme("application-x-executable")); } QWidget *AbstractOcrDialogue::addExtraEngineWidget(QWidget *wid, bool stretchBefore) { return (addExtraPageWidget(m_enginePage, wid, stretchBefore)); } void AbstractOcrDialogue::setupSpellPage() { QWidget *w = new QWidget(this); QGridLayout *gl = new QGridLayout(w); // row 0: background checking group box m_gbBackgroundCheck = new QGroupBox(i18n("Highlight misspelled words"), w); m_gbBackgroundCheck->setCheckable(true); QGridLayout *gl1 = new QGridLayout(m_gbBackgroundCheck); m_gbBackgroundCheck->setLayout(gl1); m_rbGlobalSpellSettings = new QRadioButton(i18n("Use the system spell configuration"), w); gl1->addWidget(m_rbGlobalSpellSettings, 0, 0); m_rbCustomSpellSettings = new QRadioButton(i18n("Use custom spell configuration"), w); gl1->addWidget(m_rbCustomSpellSettings, 1, 0); m_pbCustomSpellDialog = new QPushButton(i18n("Custom Spell Configuration..."), w); gl1->addWidget(m_pbCustomSpellDialog, 2, 0, Qt::AlignRight); connect(m_rbCustomSpellSettings, SIGNAL(toggled(bool)), m_pbCustomSpellDialog, SLOT(setEnabled(bool))); connect(m_pbCustomSpellDialog, SIGNAL(clicked()), SLOT(slotCustomSpellDialog())); gl->addWidget(m_gbBackgroundCheck, 0, 0); // row 1: space gl->setRowMinimumHeight(1, 2*DialogBase::verticalSpacing()); // row 2: interactive checking group box m_gbInteractiveCheck = new QGroupBox(i18n("Start interactive spell check"), w); m_gbInteractiveCheck->setCheckable(true); QGridLayout *gl2 = new QGridLayout(m_gbInteractiveCheck); m_gbInteractiveCheck->setLayout(gl2); 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); l->setWordWrap(true); gl2->addWidget(l, 0, 0); gl->addWidget(m_gbInteractiveCheck, 2, 0); // row 3: stretch gl->setRowStretch(3, 1); // Apply settings m_gbBackgroundCheck->setChecked(KookaSettings::ocrSpellBackgroundCheck()); m_gbInteractiveCheck->setChecked(KookaSettings::ocrSpellInteractiveCheck()); #ifndef KF5 const bool customSettings = KookaSettings::ocrSpellCustomSettings(); #else const bool customSettings = false; m_rbCustomSpellSettings->setEnabled(false); #endif m_rbGlobalSpellSettings->setChecked(!customSettings); m_rbCustomSpellSettings->setChecked(customSettings); m_pbCustomSpellDialog->setEnabled(customSettings); m_spellPage = addPage(w, i18n("Spell Check")); m_spellPage->setHeader(i18n("OCR Result Spell Checking")); m_spellPage->setIcon(QIcon::fromTheme("tools-check-spelling")); } void AbstractOcrDialogue::setupDebugPage() { QWidget *w = new QWidget(this); QGridLayout *gl = new QGridLayout(w); m_cbRetainFiles = new QCheckBox(i18n("Retain temporary files"), w); gl->addWidget(m_cbRetainFiles, 0, 0, Qt::AlignTop); m_cbVerboseDebug = new QCheckBox(i18n("Verbose message output"), w); gl->addWidget(m_cbVerboseDebug, 1, 0, Qt::AlignTop); gl->setRowStretch(2, 1); m_debugPage = addPage(w, i18n("Debugging")); m_debugPage->setHeader(i18n("OCR Debugging")); m_debugPage->setIcon(QIcon::fromTheme("tools-report-bug")); } void AbstractOcrDialogue::stopAnimation() { if (m_progress != nullptr) { m_progress->setVisible(false); } } void AbstractOcrDialogue::startAnimation() { if (!m_progress->isVisible()) { // progress bar not added yet m_progress->setValue(0); addExtraSetupWidget(m_progress, true); m_progress->setVisible(true); } } // Not sure why this uses an asynchronous preview job for the image thumbnail // (if it is possible, i.e. the image is file bound) as opposed to just scaling // the image (which is always loaded at this point, i.e. it is already in memory). // Possibly because scaling a potentially very large image could introduce a // significant delay in opening the dialogue box, so making the GUI appear // less responsive. So we'll keep the preview job for now. // // We now bring you a mild rant... // // What on earth happened to KFileMetaInfo in KDE4? This used to have a fairly // reasonable API, returning a list of key-value pairs grouped into sensible // categories with readable strings available for each. Now the groups have // gone (so for example methods such as preferredGroups(), albeit being marked as // 'deprecated', return an empty list!) and the key of each entry is an ontology // URL. Not sure what to do with this URL (although I'm sure it must be of // interest to something), and it doesn't even return the minimal useful // information (e.g. the size/depth) for many image file types anyway. // // Could this be why the "Meta Info" tab of the file properties dialogue also // seems to have disappeared? // // So forget about KFileMetaInfo here, just display a simple label with the // image size and depth (which information we already have available). void AbstractOcrDialogue::introduceImage(const KookaImage *img) { if (img == nullptr) { if (m_previewLabel != nullptr) { m_previewLabel->setText(i18n("No image")); } return; } //qDebug() << "url" << img->url() << "filebound" << img->isFileBound(); if (img->isFileBound()) { // image backed by a file /* Start to create a preview job for the thumb */ KFileItemList fileItems; fileItems.append(KFileItem(img->url())); KIO::PreviewJob *job = KIO::filePreview(fileItems, QSize(m_previewSize.width(), m_previewSize.height())); if (job!=nullptr) { job->setIgnoreMaximumSize(); connect(job, SIGNAL(gotPreview(KFileItem,QPixmap)), SLOT(slotGotPreview(KFileItem,QPixmap))); } } else { // selection only in memory, // do the preview ourselves QImage qimg = img->scaled(m_previewSize, Qt::KeepAspectRatio, Qt::SmoothTransformation); slotGotPreview(KFileItem(), QPixmap::fromImage(qimg)); } if (m_previewLabel != nullptr) { KLocalizedString str = img->isFileBound() ? ki18n("Image: %1") : ki18n("Selection: %1"); m_previewLabel->setText(str.subs(ImageCanvas::imageInfoString(img)).toString()); } } bool AbstractOcrDialogue::keepTempFiles() const { return (m_retainFiles); } bool AbstractOcrDialogue::verboseDebug() const { return (m_verboseDebug); } void AbstractOcrDialogue::slotGotPreview(const KFileItem &item, const QPixmap &newPix) { //qDebug() << "pixmap" << newPix.size(); if (m_previewPix != nullptr) { m_previewPix->setText(QString()); m_previewPix->setPixmap(newPix); } } void AbstractOcrDialogue::slotWriteConfig() { KookaSettings::setOcrSpellBackgroundCheck(m_gbBackgroundCheck->isChecked()); KookaSettings::setOcrSpellInteractiveCheck(m_gbInteractiveCheck->isChecked()); KookaSettings::setOcrSpellCustomSettings(m_rbCustomSpellSettings->isChecked()); KookaSettings::self()->save(); // deliberately not saving the OCR debug configuration } void AbstractOcrDialogue::slotStartOCR() { setCurrentPage(m_setupPage); // force back to first page m_retainFiles = (m_cbRetainFiles != nullptr && m_cbRetainFiles->isChecked()); m_verboseDebug = (m_cbVerboseDebug != nullptr && m_cbVerboseDebug->isChecked()); slotWriteConfig(); // save configuration emit signalOcrStart(); // start the OCR process } void AbstractOcrDialogue::enableGUI(bool running) { m_sourcePage->setEnabled(!running); m_enginePage->setEnabled(!running); if (m_spellPage != nullptr) m_spellPage->setEnabled(!running); if (m_debugPage != nullptr) m_debugPage->setEnabled(!running); enableFields(!running); // engine's GUI widgets if (running) startAnimation(); // start our progress bar else stopAnimation(); // stop our progress bar QDialogButtonBox *bb = buttonBox(); bb->button(QDialogButtonBox::Discard)->setEnabled(!running); // Start OCR bb->button(QDialogButtonBox::Apply)->setEnabled(running); // Stop OCR bb->button(QDialogButtonBox::Close)->setEnabled(!running); // Close QApplication::processEvents(); // ensure GUI up-to-date } bool AbstractOcrDialogue::wantInteractiveSpellCheck() const { return (m_gbInteractiveCheck->isChecked()); } bool AbstractOcrDialogue::wantBackgroundSpellCheck() const { return (m_gbBackgroundCheck->isChecked()); } QString AbstractOcrDialogue::customSpellConfigFile() const { if (m_rbCustomSpellSettings->isChecked()) { // our application config return (KSharedConfig::openConfig()->name()); } return ("sonnetrc"); // Sonnet global settings } QProgressBar *AbstractOcrDialogue::progressBar() const { return (m_progress); } void AbstractOcrDialogue::slotCustomSpellDialog() { #ifndef KF5 // TODO: Sonnet in KF5 appears to no longer allow a custom configuration, // QSettings("KDE","Sonnet") is hardwired in Settings::restore() in // sonnet/src/core/settings.cpp // See also KookaView::slotSetOcrSpellConfig() // It may be possible, though, to configure only the language; // see http://api.kde.org/frameworks-api/frameworks5-apidocs/sonnet/html/classSonnet_1_1ConfigDialog.html Sonnet::ConfigDialog d(this); // Sonnet::ConfigDialog d(KSharedConfig::openConfig().data(), this); d.exec(); // save to our application config #endif } diff --git a/plugins/ocr/abstractocrengine.cpp b/plugins/ocr/abstractocrengine.cpp index 85c0f7b..41d4022 100644 --- a/plugins/ocr/abstractocrengine.cpp +++ b/plugins/ocr/abstractocrengine.cpp @@ -1,596 +1,598 @@ /************************************************************************ * * * This file is part of Kooka, a scanning/OCR application using * * Qt and KDE Frameworks . * * * * Copyright (C) 2000-2016 Klaas Freitag * * Jonathan Marten * * * * Kooka is free software; you can redistribute it and/or modify it * * under the terms of the GNU Library General Public License as * * published by the Free Software Foundation and appearing in the * * file COPYING included in the packaging of this file; either * * version 2 of the License, or (at your option) any later version. * * * * As a special exception, permission is given to link this program * * with any version of the KADMOS OCR/ICR engine (a product of * * reRecognition GmbH, Kreuzlingen), and distribute the resulting * * executable without including the source code for KADMOS in the * * source distribution. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public * * License along with this program; see the file COPYING. If * * not, see . * * * ************************************************************************/ #include "abstractocrengine.h" #include #include #include #include #include #include #include #include #include #include #include #include "imagecanvas.h" #include "imageformat.h" #include "kookaimage.h" #include "abstractocrdialogue.h" // Constructor/destructor and external engine creation // --------------------------------------------------- AbstractOcrEngine::AbstractOcrEngine(QObject *pnt, const char *name) : AbstractPlugin(pnt), m_ocrProcess(nullptr), m_ocrRunning(false), m_ocrDialog(nullptr), m_resultImage(nullptr), m_imgCanvas(nullptr), m_document(nullptr), m_cursor(nullptr), m_currHighlight(-1), m_trackingActive(false) { setObjectName(name); m_introducedImage = KookaImage(); m_parent = nullptr; qDebug() << objectName(); } AbstractOcrEngine::~AbstractOcrEngine() { qDebug() << objectName(); if (m_ocrProcess!=nullptr) delete m_ocrProcess; if (m_ocrDialog!=nullptr) delete m_ocrDialog; } /* * This is called to introduce a new image, usually if the user clicks on a * new image either in the gallery or on the thumbnailview. */ void AbstractOcrEngine::setImage(const KookaImage &img) { m_introducedImage = img; // shallow copy of original if (m_ocrDialog!=nullptr) m_ocrDialog->introduceImage(&m_introducedImage); m_trackingActive = false; } /* * Starts the visual OCR process. Depending on the OCR engine, this function creates * a new dialog, and shows it. */ bool AbstractOcrEngine::openOcrDialogue(QWidget *pnt) { if (m_ocrRunning) { KMessageBox::sorry(pnt, i18n("OCR is already in progress")); return (false); } m_parent = pnt; m_errorText.clear(); // ready for new messages m_ocrDialog = createOcrDialogue(this, pnt); Q_ASSERT(m_ocrDialog!=nullptr); if (!m_ocrDialog->setupGui()) { const QString msg = collectErrorMessages(i18n("OCR could not be started."), i18n("Check the OCR engine selection and settings.")); int result = KMessageBox::warningContinueCancel(pnt, msg, i18n("OCR Setup Error"), KGuiItem(i18n("Configure OCR..."))); if (result==KMessageBox::Continue) emit openOcrPrefs(); return (false); // with no OCR dialogue } connect(m_ocrDialog, &AbstractOcrDialogue::signalOcrStart, this, &AbstractOcrEngine::slotStartOCR); connect(m_ocrDialog, &AbstractOcrDialogue::signalOcrStop, this, &AbstractOcrEngine::slotStopOCR); connect(m_ocrDialog, &QDialog::rejected, this, &AbstractOcrEngine::slotClose); m_ocrDialog->introduceImage(&m_introducedImage); m_ocrDialog->show(); // TODO: m_ocrActive would better reflect the function (if indeed useful at all) m_ocrRunning = true; return (true); } /* Called by "Close" used while OCR is not in progress */ void AbstractOcrEngine::slotClose() { stopOcrProcess(false); } /* Called by "Stop" used while OCR is in progress */ void AbstractOcrEngine::slotStopOCR() { Q_ASSERT(m_ocrDialog!=nullptr); stopOcrProcess(true); m_ocrDialog->enableGUI(false); // enable controls again } /* Called by "Start" used while OCR is not in progress */ void AbstractOcrEngine::slotStartOCR() { Q_ASSERT(m_ocrDialog!=nullptr); m_ocrDialog->enableGUI(true); // disable controls while running m_ocrDialog->show(); // just in case it got closed createOcrProcess(m_ocrDialog, &m_introducedImage); } void AbstractOcrEngine::stopOcrProcess(bool tellUser) { if (m_ocrProcess!=nullptr && m_ocrProcess->state()==QProcess::Running) { qDebug() << "Killing OCR process" << m_ocrProcess->pid(); m_ocrProcess->kill(); if (tellUser) KMessageBox::error(m_parent, i18n("The OCR process was stopped")); } finishedOcr(false); } /** * This method should be called by the engine specific finish slots. * It does the engine independent cleanups like re-enabling buttons etc. */ void AbstractOcrEngine::finishedOcr(bool success) { if (m_ocrDialog!=nullptr) m_ocrDialog->enableGUI(false); if (success) { emit newOCRResultText(); // send out the text result if (!m_ocrResultFile.isEmpty() && // there is a result image m_imgCanvas!=nullptr) // and we can display it { delete m_resultImage; // create new result image m_resultImage = new QImage(m_ocrResultFile); qDebug() << "Result image" << m_ocrResultFile << "size" << m_resultImage->size(); m_imgCanvas->newImage(m_resultImage, true); // display on image canvas m_imgCanvas->setReadOnly(true); m_trackingActive = true; // handle clicks on image } /* now it is time to invoke the dictionary if required */ // TODO: readOnlyEditor needed here? Also done in finishResultDocument() emit readOnlyEditor(false); // user can now edit if (m_ocrDialog != nullptr) { emit setSpellCheckConfig(m_ocrDialog->customSpellConfigFile()); bool doSpellcheck = m_ocrDialog->wantInteractiveSpellCheck(); bool bgSpellcheck = m_ocrDialog->wantBackgroundSpellCheck(); emit startSpellCheck(doSpellcheck, bgSpellcheck); } } if (m_ocrDialog!=nullptr) m_ocrDialog->hide(); // close the dialogue m_ocrRunning = false; removeTempFiles(); qDebug() << "OCR finished"; } void AbstractOcrEngine::removeTempFiles() { bool retain = m_ocrDialog->keepTempFiles(); qDebug() << "retain=" << retain; QStringList temps = tempFiles(retain); // get files used by engine if (!m_ocrResultFile.isEmpty()) temps << m_ocrResultFile; // plus our result image if (!m_ocrStderrLog.isEmpty()) temps << m_ocrStderrLog; // and our standard error log if (temps.join("").isEmpty()) return; // no temporary files to remove if (retain) { QString s = xi18nc("@info", "The following OCR temporary files are retained for debugging:"); for (QStringList::const_iterator it = temps.constBegin(); it != temps.constEnd(); ++it) { const QString file = (*it); if (file.isEmpty()) continue; QUrl u = QUrl::fromLocalFile(file); s += xi18nc("@info", "%2", u.url(), file); } if (KMessageBox::questionYesNo(m_parent, s, i18n("OCR Temporary Files"), KStandardGuiItem::del(), KStandardGuiItem::close(), QString(), KMessageBox::AllowLink)==KMessageBox::Yes) retain = false; } if (!retain) { for (QStringList::const_iterator it = temps.constBegin(); it != temps.constEnd(); ++it) { if ((*it).isEmpty()) { continue; } QString tf = (*it); QFileInfo fi(tf); if (!fi.exists()) { // what happened? //qDebug() << "does not exist:" << tf; } else if (fi.isDir()) { //qDebug() << "temp dir" << tf; QDir(tf).removeRecursively(); // recursive deletion } else { //qDebug() << "temp file" << tf; QFile::remove(tf); // just a simple file } } } } // Filtering mouse events on the image viewer // ------------------------------------------ void AbstractOcrEngine::setImageCanvas(ImageCanvas *canvas) { m_imgCanvas = canvas; connect(m_imgCanvas, &ImageCanvas::doubleClicked, this, &AbstractOcrEngine::slotImagePosition); } void AbstractOcrEngine::slotImagePosition(const QPoint &p) { if (!m_trackingActive) return; // not interested // ImageCanvas did the coordinate conversion. // OcrResEdit does all of the rest of the work. emit selectWord(p); } // Highlighting/scrolling the result text // -------------------------------------- void AbstractOcrEngine::slotHighlightWord(const QRect &r) { if (m_imgCanvas == nullptr) { return; } if (m_currHighlight > -1) { m_imgCanvas->removeHighlight(m_currHighlight); } m_currHighlight = -1; if (!m_trackingActive) { return; // not highlighting } if (!r.isValid()) { return; // word rectangle invalid } KColorScheme sch(QPalette::Active, KColorScheme::Selection); QColor col = sch.background(KColorScheme::NegativeBackground).color(); m_imgCanvas->setHighlightStyle(ImageCanvas::HighlightBox, QPen(col, 2)); m_currHighlight = m_imgCanvas->addHighlight(r, true); } void AbstractOcrEngine::slotScrollToWord(const QRect &r) { if (m_imgCanvas == nullptr) { return; } if (m_currHighlight > -1) { m_imgCanvas->removeHighlight(m_currHighlight); } m_currHighlight = -1; if (!m_trackingActive) { return; // not highlighting } KColorScheme sch(QPalette::Active, KColorScheme::Selection); QColor col = sch.background(KColorScheme::NeutralBackground).color(); m_imgCanvas->setHighlightStyle(ImageCanvas::HighlightUnderline, QPen(col, 2)); m_currHighlight = m_imgCanvas->addHighlight(r, true); } // Assembling the OCR results // -------------------------- void AbstractOcrEngine::setTextDocument(QTextDocument *doc) { m_document = doc; } QTextDocument *AbstractOcrEngine::startResultDocument() { m_document->setUndoRedoEnabled(false); m_document->clear(); m_wordCount = 0; m_cursor = new QTextCursor(m_document); emit readOnlyEditor(true); // read only while updating return (m_document); } void AbstractOcrEngine::finishResultDocument() { qDebug() << "words" << m_wordCount << "lines" << m_document->blockCount() << "chars" << m_document->characterCount(); if (m_cursor != nullptr) delete m_cursor; emit readOnlyEditor(false); // now let user edit it } void AbstractOcrEngine::startLine() { if (verboseDebug()) { //qDebug(); } if (!m_cursor->atStart()) { m_cursor->insertBlock(QTextBlockFormat(), QTextCharFormat()); } } void AbstractOcrEngine::finishLine() { } void AbstractOcrEngine::addWord(const QString &word, const OcrWordData &data) { if (verboseDebug()) { //qDebug() << "word" << word << "len" << word.length() //<< "rect" << data.property(OcrWordData::Rectangle) //<< "alts" << data.property(OcrWordData::Alternatives); } if (!m_cursor->atBlockStart()) { m_cursor->insertText(" ", QTextCharFormat()); } m_cursor->insertText(word, data); ++m_wordCount; } QString AbstractOcrEngine::tempFileName(const QString &suffix, const QString &baseName) { - const QString protoName = QDir::tempPath()+'/'+baseName+"_XXXXXX."+suffix; + QString protoName = QDir::tempPath()+'/'+baseName+"_XXXXXX"; + if (!suffix.isEmpty()) protoName += "."+suffix; + QTemporaryFile tmpFile(protoName); tmpFile.setAutoRemove(false); if (!tmpFile.open()) { qDebug() << "error creating temporary file" << protoName; setErrorText(xi18nc("@info", "Cannot create temporary file %1", protoName)); return (QString()); } QString tmpName = QFile::encodeName(tmpFile.fileName()); tmpFile.close(); // just want its name return (tmpName); } QString AbstractOcrEngine::tempSaveImage(const KookaImage *img, const ImageFormat &format, int colors) { if (img==nullptr) return (QString()); // no image to save QString tmpName = tempFileName(format.extension(), "imagetemp"); const KookaImage *tmpImg = nullptr; if (colors!=-1 && img->depth()!=colors) // need to convert image { QImage::Format newfmt; switch (colors) { case 1: newfmt = QImage::Format_Mono; break; case 8: newfmt = QImage::Format_Indexed8; break; case 24: newfmt = QImage::Format_RGB888; break; case 32: newfmt = QImage::Format_RGB32; break; default: qWarning() << "bad colour depth" << colors; return (QString()); } tmpImg = new KookaImage(img->convertToFormat(newfmt)); img = tmpImg; // replace with converted image } qDebug() << "saving to" << tmpName << "in format" << format; if (!img->save(tmpName, format.name())) { qDebug() << "Error saving to" << tmpName; setErrorText(xi18nc("@info", "Cannot save image to temporary file %1", tmpName)); tmpName.clear(); } if (tmpImg!=nullptr) delete tmpImg; return (tmpName); } bool AbstractOcrEngine::verboseDebug() const { return (m_ocrDialog->verboseDebug()); } QString AbstractOcrEngine::findExecutable(QString (*settingFunc)(), KConfigSkeletonItem *settingItem) { QString exec = (*settingFunc)(); // get current setting if (exec.isEmpty()) settingItem->setDefault(); // if null, apply default exec = (*settingFunc)(); // and get new setting Q_ASSERT(!exec.isEmpty()); // should now have something qDebug() << "configured/default" << exec; if (!QDir::isAbsolutePath(exec)) // not specified absolute path { const QString pathExec = QStandardPaths::findExecutable(exec); if (pathExec.isEmpty()) // try to find executable { qDebug() << "no" << exec << "found on PATH"; setErrorText(xi18nc("@info", "The executable %1 could not be found on PATH.")); return (QString()); } exec = pathExec; } QFileInfo fi(exec); // now check it is usable if (!fi.exists() || fi.isDir() || !fi.isExecutable()) { qDebug() << "configured" << exec << "not usable"; setErrorText(xi18nc("@info", "The executable %1 does not exist or is not usable.", fi.absoluteFilePath())); return (QString()); } qDebug() << "found" << exec; return (exec); } QString AbstractOcrEngine::collectErrorMessages(const QString &starter, const QString &ender) { // Any error message(s) in m_errorText will already have been converted // from KUIT markup to HTML by xi18nc() or similar. So all that is // needed is to build the rest of the error message in rich text also. // There will be some spurious tags around each separate message // which has been converted, but they don't seem to cause any problem. m_errorText.prepend(QString()); m_errorText.prepend(starter); m_errorText.prepend(""); m_errorText.append(QString()); m_errorText.append(ender); m_errorText.append(""); return (m_errorText.join("
")); } QProcess *AbstractOcrEngine::initOcrProcess() { if (m_ocrProcess!=nullptr) delete m_ocrProcess; // kill old process if still there m_ocrProcess = new QProcess(); // start new OCR process Q_CHECK_PTR(m_ocrProcess); qDebug(); m_ocrProcess->setStandardInputFile(QProcess::nullDevice()); m_ocrProcess->setProcessChannelMode(QProcess::SeparateChannels); m_ocrStderrLog = tempFileName("stderr.log"); m_ocrProcess->setStandardErrorFile(m_ocrStderrLog); return (m_ocrProcess); } bool AbstractOcrEngine::runOcrProcess() { qDebug() << "Running OCR," << m_ocrProcess->program() << m_ocrProcess->arguments(); connect(m_ocrProcess, QOverload::of(&QProcess::finished), this, &AbstractOcrEngine::slotProcessExited); m_ocrProcess->start(); if (!m_ocrProcess->waitForStarted(5000)) { qWarning() << "Error starting OCR process"; return (false); } return (true); } void AbstractOcrEngine::slotProcessExited(int exitCode, QProcess::ExitStatus exitStatus) { qDebug() << "exit code" << exitCode << "status" << exitStatus; bool success = (exitStatus==QProcess::NormalExit && exitCode==0); if (!success) // OCR command failed { if (exitStatus==QProcess::CrashExit) { setErrorText(xi18nc("@info", "Command %1 crashed with exit status %2", m_ocrProcess->program(), exitCode)); } else { setErrorText(xi18nc("@info", "Command %1 exited with status %2", m_ocrProcess->program(), exitCode)); } const QString msg = collectErrorMessages(xi18nc("@info", "Running the OCR process failed."), xi18nc("@info", "More information may be available in its standard error log file.", QUrl::fromLocalFile(m_ocrStderrLog).url())); KMessageBox::sorry(m_parent, msg, i18n("OCR Command Failed"), KMessageBox::AllowLink); } else // OCR command succeeded { success = finishedOcrProcess(m_ocrProcess); // process the OCR results if (!success) // OCR processing failed { const QString msg = collectErrorMessages(xi18nc("@info", "Processing the OCR results failed."), QString()); KMessageBox::sorry(m_parent, msg, i18n("OCR Processing Failed"), KMessageBox::AllowLink); } } finishedOcr(success); } diff --git a/plugins/ocr/abstractocrengine.h b/plugins/ocr/abstractocrengine.h index cd2a8de..5ed7576 100644 --- a/plugins/ocr/abstractocrengine.h +++ b/plugins/ocr/abstractocrengine.h @@ -1,239 +1,239 @@ /************************************************************************ * * * This file is part of Kooka, a scanning/OCR application using * * Qt and KDE Frameworks . * * * * Copyright (C) 2000-2018 Klaas Freitag * * Jonathan Marten * * * * Kooka is free software; you can redistribute it and/or modify it * * under the terms of the GNU Library General Public License as * * published by the Free Software Foundation and appearing in the * * file COPYING included in the packaging of this file; either * * version 2 of the License, or (at your option) any later version. * * * * As a special exception, permission is given to link this program * * with any version of the KADMOS OCR/ICR engine (a product of * * reRecognition GmbH, Kreuzlingen), and distribute the resulting * * executable without including the source code for KADMOS in the * * source distribution. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public * * License along with this program; see the file COPYING. If * * not, see . * * * ************************************************************************/ #ifndef ABSTRACTOCRENGINE_H #define ABSTRACTOCRENGINE_H #include #include #include #include "kookaimage.h" #include "abstractplugin.h" /** *@author Klaas Freitag */ class QProcess; class KConfigSkeletonItem; class ImageFormat; class ImageCanvas; class AbstractOcrDialogue; #ifndef PLUGIN_EXPORT #define PLUGIN_EXPORT #endif // Using a QTextDocument for the OCR results, with the source image rectangle // and other OCR information held as text properties. class OcrWordData : public QTextCharFormat { public: enum DataType { Rectangle = QTextFormat::UserProperty, // QRect Alternatives, // QStringList - KNode // int + KNode // int - not sure what this ever did }; OcrWordData() : QTextCharFormat() {} }; class PLUGIN_EXPORT AbstractOcrEngine : public AbstractPlugin { Q_OBJECT public: virtual ~AbstractOcrEngine(); bool openOcrDialogue(QWidget *pnt = nullptr); /** * Sets an image canvas that displays the result image of the OCR. * If this is set to @c nullptr (or never set) no result image is displayed. * The OCR fabric passes a new image to the canvas which is a copy of * the image to OCR. */ void setImageCanvas(ImageCanvas *canvas); void setImage(const KookaImage &img); void setTextDocument(QTextDocument *doc); QString findExecutable(QString (*settingFunc)(), KConfigSkeletonItem *settingItem); void setErrorText(const QString &msg) { m_errorText.append(msg); } /** * Check whether the engine has advanced settings: for example, the * pathname of an executable which performs the OCR. The actual dialogue * will be requested by @c openAdvancedSettings(). * * @return @c true if the engine has advanced settings **/ virtual bool hasAdvancedSettings() const { return (false); } /** * Open a dialogue for advanced engine settings. This will only be * called if the engine has indicated that it has advanced settings, * by returning @c true from @c hasAdvancedSettings(). **/ virtual void openAdvancedSettings() {} protected: explicit AbstractOcrEngine(QObject *pnt, const char *name); virtual AbstractOcrDialogue *createOcrDialogue(AbstractOcrEngine *plugin, QWidget *pnt) = 0; virtual QStringList tempFiles(bool retain) = 0; /** * Save an image to a temporary file. * * @param img The image to save * @param format The image format to save in * @param colors The colour depth (bits per pixel) required. If specified, * this must be either 1, 8, 24 or 32. The default is for no colour * conversion. * * @return The file name as saved, or @c QString() if there was * an error. **/ QString tempSaveImage(const KookaImage *img, const ImageFormat &format, int colors = -1); /** * Get a name to use for a temporary file. * * @param suffix File name suffix, no leading '.' is required * @return The temporary file name, or @c QString() if the file could not be created * * @note The temporary file is created and is left in place under the returned name, * but is not opened. Its name should be saved and eventually returned in the * @c tempFiles() list so that it will be removed. **/ QString tempFileName(const QString &suffix, const QString &baseName = "ocrtemp"); QTextDocument *startResultDocument(); void finishResultDocument(); void startLine(); void addWord(const QString &word, const OcrWordData &data); void finishLine(); bool verboseDebug() const; virtual bool createOcrProcess(AbstractOcrDialogue *dia, const KookaImage *img) = 0; QProcess *initOcrProcess(); QProcess *ocrProcess() const { return (m_ocrProcess); } bool runOcrProcess(); virtual bool finishedOcrProcess(QProcess *proc) { Q_UNUSED(proc); return (true); } void setResultImage(const QString &file) { m_ocrResultFile = file; } signals: void newOCRResultText(); void openOcrPrefs(); void setSpellCheckConfig(const QString &configFile); void startSpellCheck(bool interactive, bool background); /** * Indicates that the text editor holding the text that came through * newOCRResultText should be set to readonly or not. Can be connected * to QTextEdit::setReadOnly directly. */ void readOnlyEditor(bool isReadOnly); /** * Progress of the OCR process. The first integer is the main progress, * the second the sub progress. If there is only one progress, it is the * first parameter while the second should be -1. * Both have a range from 0..100. * * Note that this signal may not be emitted if the engine does not support * progress. */ void ocrProgress(int progress, int subprogress); /** * Select a word in the editor corresponding to the position within * the result image. */ void selectWord(const QPoint &p); public slots: void slotHighlightWord(const QRect &r); void slotScrollToWord(const QRect &r); private slots: void slotStartOCR(); void slotStopOCR(); void slotClose(); void slotProcessExited(int exitCode, QProcess::ExitStatus exitStatus); /** * Handle mouse double clicks on the image viewer showing the * OCR result image. */ void slotImagePosition(const QPoint &p); private: void stopOcrProcess(bool tellUser); void removeTempFiles(); void finishedOcr(bool success); QString collectErrorMessages(const QString &starter, const QString &ender); private: QWidget *m_parent; QProcess *m_ocrProcess; bool m_ocrRunning; AbstractOcrDialogue *m_ocrDialog; QStringList m_errorText; QString m_ocrStderrLog; QString m_ocrResultFile; KookaImage m_introducedImage; QImage *m_resultImage; ImageCanvas *m_imgCanvas; QTextDocument *m_document; QTextCursor *m_cursor; int m_currHighlight; bool m_trackingActive; int m_wordCount; }; #endif // ABSTRACTOCRENGINE_H diff --git a/plugins/ocr/gocr/CMakeLists.txt b/plugins/ocr/gocr/CMakeLists.txt index dbf8239..a3c4f71 100644 --- a/plugins/ocr/gocr/CMakeLists.txt +++ b/plugins/ocr/gocr/CMakeLists.txt @@ -1,37 +1,38 @@ ########################################################################## ## ## ## This CMake file is part of Kooka, a KDE scanning/OCR application. ## ## ## ## This file may be distributed and/or modified under the terms of ## ## the GNU General Public License version 2, as published by the ## ## Free Software Foundation and appearing in the file COPYING ## ## included in the packaging of this file. ## ## ## ## Author: Jonathan Marten ## ## ## ########################################################################## project(kooka5) ######################################################################### # # # OCR plugin for GOCR # # # ######################################################################### set(kookaocrgocr_SRCS ocrgocrengine.cpp ocrgocrdialog.cpp ) add_library(kookaocrgocr MODULE ${kookaocrgocr_SRCS}) kcoreaddons_desktop_to_json(kookaocrgocr kookaocr-gocr.desktop) target_link_libraries(kookaocrgocr Qt5::Core Qt5::Gui) target_link_libraries(kookaocrgocr KF5::I18n KF5::WidgetsAddons) target_link_libraries(kookaocrgocr kookaocr kookascan) if (INSTALL_BINARIES) install(TARGETS kookaocrgocr DESTINATION ${PLUGIN_INSTALL_DIR}/kooka) endif (INSTALL_BINARIES) install(FILES kookaocr-gocr.desktop DESTINATION ${SERVICES_INSTALL_DIR}) +install(FILES gocr.png DESTINATION ${PICS_INSTALL_DIR}) diff --git a/app/pics/gocr.png b/plugins/ocr/gocr/gocr.png similarity index 100% rename from app/pics/gocr.png rename to plugins/ocr/gocr/gocr.png diff --git a/plugins/ocr/gocr/ocrgocrengine.h b/plugins/ocr/gocr/ocrgocrengine.h index f39c6af..47dfcba 100644 --- a/plugins/ocr/gocr/ocrgocrengine.h +++ b/plugins/ocr/gocr/ocrgocrengine.h @@ -1,69 +1,67 @@ /************************************************************************ * * * This file is part of Kooka, a scanning/OCR application using * * Qt and KDE Frameworks . * * * * Copyright (C) 2000-2016 Klaas Freitag * * Jonathan Marten * * * * Kooka is free software; you can redistribute it and/or modify it * * under the terms of the GNU Library General Public License as * * published by the Free Software Foundation and appearing in the * * file COPYING included in the packaging of this file; either * * version 2 of the License, or (at your option) any later version. * * * * As a special exception, permission is given to link this program * * with any version of the KADMOS OCR/ICR engine (a product of * * reRecognition GmbH, Kreuzlingen), and distribute the resulting * * executable without including the source code for KADMOS in the * * source distribution. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public * * License along with this program; see the file COPYING. If * * not, see . * * * ************************************************************************/ #ifndef OCRGOCRENGINE_H #define OCRGOCRENGINE_H -#include - #include "abstractocrengine.h" class QTemporaryDir; class OcrGocrEngine : public AbstractOcrEngine { Q_OBJECT public: explicit OcrGocrEngine(QObject *pnt, const QVariantList &args); ~OcrGocrEngine() override = default; AbstractOcrDialogue *createOcrDialogue(AbstractOcrEngine *plugin, QWidget *pnt) override; bool hasAdvancedSettings() const override { return (true); } void openAdvancedSettings() override; protected: bool createOcrProcess(AbstractOcrDialogue *dia, const KookaImage *img) override; QStringList tempFiles(bool retain) override; bool finishedOcrProcess(QProcess *proc) override; protected slots: void slotGOcrStdout(); private: QTemporaryDir *m_tempDir; QString m_inputFile; QString m_resultFile; }; #endif // OCRGOCRENGINE_H diff --git a/plugins/ocr/ocrad/CMakeLists.txt b/plugins/ocr/ocrad/CMakeLists.txt index 928be00..59d0e39 100644 --- a/plugins/ocr/ocrad/CMakeLists.txt +++ b/plugins/ocr/ocrad/CMakeLists.txt @@ -1,37 +1,38 @@ ########################################################################## ## ## ## This CMake file is part of Kooka, a KDE scanning/OCR application. ## ## ## ## This file may be distributed and/or modified under the terms of ## ## the GNU General Public License version 2, as published by the ## ## Free Software Foundation and appearing in the file COPYING ## ## included in the packaging of this file. ## ## ## ## Author: Jonathan Marten ## ## ## ########################################################################## project(kooka5) ######################################################################### # # # OCR plugin for OCRAD # # # ######################################################################### set(kookaocrocrad_SRCS ocrocradengine.cpp ocrocraddialog.cpp ) add_library(kookaocrocrad MODULE ${kookaocrocrad_SRCS}) kcoreaddons_desktop_to_json(kookaocrocrad kookaocr-ocrad.desktop) target_link_libraries(kookaocrocrad Qt5::Core Qt5::Gui) target_link_libraries(kookaocrocrad KF5::I18n KF5::WidgetsAddons) target_link_libraries(kookaocrocrad kookaocr kookascan) if (INSTALL_BINARIES) install(TARGETS kookaocrocrad DESTINATION ${PLUGIN_INSTALL_DIR}/kooka) endif (INSTALL_BINARIES) install(FILES kookaocr-ocrad.desktop DESTINATION ${SERVICES_INSTALL_DIR}) +install(FILES ocrad.png DESTINATION ${PICS_INSTALL_DIR}) diff --git a/app/pics/ocrad.png b/plugins/ocr/ocrad/ocrad.png similarity index 100% rename from app/pics/ocrad.png rename to plugins/ocr/ocrad/ocrad.png diff --git a/plugins/ocr/ocrad/ocrocradengine.h b/plugins/ocr/ocrad/ocrocradengine.h index f6e1ca8..f26e3a9 100644 --- a/plugins/ocr/ocrad/ocrocradengine.h +++ b/plugins/ocr/ocrad/ocrocradengine.h @@ -1,63 +1,61 @@ /***************************************************** -*- mode:c++; -*- *** ------------------- begin : Fri Jun 30 2000 copyright : (C) 2000 by Klaas Freitag email : freitag@suse.de ***************************************************************************/ /*************************************************************************** * * * This file may be distributed and/or modified under the terms of the * * GNU General Public License version 2 as published by the Free Software * * Foundation and appearing in the file COPYING included in the * * packaging of this file. * * * As a special exception, permission is given to link this program * * with any version of the KADMOS ocr/icr engine of reRecognition GmbH, * * Kreuzlingen and distribute the resulting executable without * * including the source code for KADMOS in the source distribution. * * * As a special exception, permission is given to link this program * * with any edition of Qt, and distribute the resulting executable, * * without including the source code for Qt in the source distribution. * * * ***************************************************************************/ #ifndef OCROCRADENGINE_H #define OCROCRADENGINE_H -#include - #include "abstractocrengine.h" class OcrOcradEngine : public AbstractOcrEngine { Q_OBJECT public: explicit OcrOcradEngine(QObject *pnt, const QVariantList &args); ~OcrOcradEngine() override = default; AbstractOcrDialogue *createOcrDialogue(AbstractOcrEngine *plugin, QWidget *pnt) override; bool hasAdvancedSettings() const override { return (true); } void openAdvancedSettings() override; protected: bool createOcrProcess(AbstractOcrDialogue *dia, const KookaImage *img) override; QStringList tempFiles(bool retain) override; bool finishedOcrProcess(QProcess *proc) override; private: QString readORF(const QString &fileName); private: QString m_ocrImagePBM; QString m_tempOrfName; QString m_tempStdoutLog; int ocradVersion; }; #endif // OCROCRADENGINE_H diff --git a/plugins/ocr/ocrad/CMakeLists.txt b/plugins/ocr/tesseract/CMakeLists.txt similarity index 56% copy from plugins/ocr/ocrad/CMakeLists.txt copy to plugins/ocr/tesseract/CMakeLists.txt index 928be00..2ed81bf 100644 --- a/plugins/ocr/ocrad/CMakeLists.txt +++ b/plugins/ocr/tesseract/CMakeLists.txt @@ -1,37 +1,38 @@ ########################################################################## ## ## ## This CMake file is part of Kooka, a KDE scanning/OCR application. ## ## ## ## This file may be distributed and/or modified under the terms of ## ## the GNU General Public License version 2, as published by the ## ## Free Software Foundation and appearing in the file COPYING ## ## included in the packaging of this file. ## ## ## ## Author: Jonathan Marten ## ## ## ########################################################################## project(kooka5) ######################################################################### # # -# OCR plugin for OCRAD # +# OCR plugin for Tesseract # # # ######################################################################### -set(kookaocrocrad_SRCS - ocrocradengine.cpp - ocrocraddialog.cpp +set(kookaocrtesseract_SRCS + ocrtesseractengine.cpp + ocrtesseractdialog.cpp ) -add_library(kookaocrocrad MODULE ${kookaocrocrad_SRCS}) -kcoreaddons_desktop_to_json(kookaocrocrad kookaocr-ocrad.desktop) +add_library(kookaocrtesseract MODULE ${kookaocrtesseract_SRCS}) +kcoreaddons_desktop_to_json(kookaocrtesseract kookaocr-tesseract.desktop) -target_link_libraries(kookaocrocrad Qt5::Core Qt5::Gui) -target_link_libraries(kookaocrocrad KF5::I18n KF5::WidgetsAddons) -target_link_libraries(kookaocrocrad kookaocr kookascan) +target_link_libraries(kookaocrtesseract Qt5::Core Qt5::Gui) +target_link_libraries(kookaocrtesseract KF5::I18n KF5::WidgetsAddons) +target_link_libraries(kookaocrtesseract kookaocr kookascan) if (INSTALL_BINARIES) - install(TARGETS kookaocrocrad DESTINATION ${PLUGIN_INSTALL_DIR}/kooka) + install(TARGETS kookaocrtesseract DESTINATION ${PLUGIN_INSTALL_DIR}/kooka) endif (INSTALL_BINARIES) -install(FILES kookaocr-ocrad.desktop DESTINATION ${SERVICES_INSTALL_DIR}) +install(FILES kookaocr-tesseract.desktop DESTINATION ${SERVICES_INSTALL_DIR}) +install(FILES tesseract.png DESTINATION ${PICS_INSTALL_DIR}) diff --git a/plugins/ocr/tesseract/kookaocr-tesseract.desktop b/plugins/ocr/tesseract/kookaocr-tesseract.desktop new file mode 100644 index 0000000..0b1d9a6 --- /dev/null +++ b/plugins/ocr/tesseract/kookaocr-tesseract.desktop @@ -0,0 +1,9 @@ +[Desktop Entry] +Type=Service +X-KDE-ServiceTypes=Kooka/OcrPlugin +X-KDE-Library=kookaocrtesseract +X-KDE-PluginInfo-Name=tesseract +X-KDE-PluginInfo-License=GPL +Icon=tesseract +Name=Tesseract +Comment=Tesseract is a free software OCR engine, originally developed at at HP Labs and now sponsored by Google. It supports multiple languages and scripts (including right-to-left text) and page layout analysis.The best results are achieved if scanned characters are at least 20 pixels high. Problems may arise with excessively skewed or rotated images, or if dark borders are present.See github.com/tesseract-ocr/tesseract for more information on Tesseract. diff --git a/plugins/ocr/tesseract/ocrtesseractdialog.cpp b/plugins/ocr/tesseract/ocrtesseractdialog.cpp new file mode 100644 index 0000000..dd75272 --- /dev/null +++ b/plugins/ocr/tesseract/ocrtesseractdialog.cpp @@ -0,0 +1,319 @@ +/************************************************************************ + * * + * This file is part of Kooka, a scanning/OCR application using * + * Qt and KDE Frameworks . * + * * + * Copyright (C) 2020 Jonathan Marten * + * * + * Kooka is free software; you can redistribute it and/or modify it * + * under the terms of the GNU Library General Public License as * + * published by the Free Software Foundation and appearing in the * + * file COPYING included in the packaging of this file; either * + * version 2 of the License, or (at your option) any later version. * + * * + * As a special exception, permission is given to link this program * + * with any version of the KADMOS OCR/ICR engine (a product of * + * reRecognition GmbH, Kreuzlingen), and distribute the resulting * + * executable without including the source code for KADMOS in the * + * source distribution. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public * + * License along with this program; see the file COPYING. If * + * not, see . * + * * + ************************************************************************/ + +#include "ocrtesseractdialog.h" + +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "kookaimage.h" +#include "kookapref.h" +#include "kookasettings.h" +#include "dialogbase.h" + +#include "ocrtesseractengine.h" + + +OcrTesseractDialog::OcrTesseractDialog(AbstractOcrEngine *plugin, QWidget *pnt) + : AbstractOcrDialogue(plugin, pnt), + m_setupWidget(nullptr), + m_ocrCmd(QString()), + m_versionNum(0), + m_versionStr(QString()) +{ +} + + +bool OcrTesseractDialog::setupGui() +{ + AbstractOcrDialogue::setupGui(); // build the standard GUI + + // Options available vary with the Tesseract version. So we need to find + // the Tesseract binary and get its version before creating the GUI. + m_ocrCmd = engine()->findExecutable(&KookaSettings::ocrTesseractBinary, KookaSettings::self()->ocrTesseractBinaryItem()); + + if (!m_ocrCmd.isEmpty()) getVersion(m_ocrCmd); // found, get its version + else // not found or invalid + { + engine()->setErrorText(i18n("The Tesseract executable is not configured or is not available.")); + } + + QWidget *w = addExtraSetupWidget(); + QGridLayout *gl = new QGridLayout(w); + int row = 0; + + // Language, auto detected values + QMap vals = getValidValues("list-langs"); + KConfigSkeletonItem *ski = KookaSettings::self()->ocrTesseractLanguageItem(); + Q_ASSERT(ski!=nullptr); + QLabel *l = new QLabel(ski->label(), w); + gl->addWidget(l, row, 0); + m_language = new QComboBox(w); + m_language->setToolTip(ski->toolTip()); + m_language->addItem(i18n("(default)"), QString()); + for (QMap::const_iterator it = vals.constBegin(); it!=vals.constEnd(); ++it) + { + m_language->addItem((!it.value().isEmpty() ? it.value() : it.key()), it.key()); + } + + if (vals.isEmpty()) m_language->setEnabled(false); + else + { + int ix = m_language->findData(KookaSettings::ocrTesseractLanguage()); + if (ix!=-1) m_language->setCurrentIndex(ix); + } + + gl->addWidget(m_language, row, 1); + l->setBuddy(m_language); + ++row; + + // User words, from file + ski = KookaSettings::self()->ocrTesseractUserWordsItem(); + Q_ASSERT(ski!=nullptr); + l = new QLabel(ski->label(), w); + gl->addWidget(l, row, 0); + m_userWords = new KUrlRequester(w); + m_userWords->setAcceptMode(QFileDialog::AcceptOpen); + m_userWords->setMode(KFile::File|KFile::ExistingOnly|KFile::LocalOnly); + m_userWords->setPlaceholderText(i18n("Select a file if required...")); + + QUrl u = KookaSettings::ocrTesseractUserWords(); + if (u.isValid()) m_userWords->setUrl(u); + + gl->addWidget(m_userWords, row, 1); + l->setBuddy(m_userWords); + ++row; + + // User patterns, from file + ski = KookaSettings::self()->ocrTesseractUserPatternsItem(); + Q_ASSERT(ski!=nullptr); + l = new QLabel(ski->label(), w); + gl->addWidget(l, row, 0); + m_userPatterns = new KUrlRequester(w); + m_userPatterns->setAcceptMode(QFileDialog::AcceptOpen); + m_userPatterns->setMode(KFile::File|KFile::ExistingOnly|KFile::LocalOnly); + m_userPatterns->setPlaceholderText(i18n("Select a file if required...")); + + u = KookaSettings::ocrTesseractUserPatterns(); + if (u.isValid()) m_userPatterns->setUrl(u); + + gl->addWidget(m_userPatterns, row, 1); + l->setBuddy(m_userPatterns); + ++row; + + gl->setRowMinimumHeight(row, DialogBase::verticalSpacing()); + ++row; + + // Page segmentation mode, auto detected values + vals = getValidValues("help-psm"); + ski = KookaSettings::self()->ocrTesseractSegmentationModeItem(); + Q_ASSERT(ski!=nullptr); + l = new QLabel(ski->label(), w); + gl->addWidget(l, row, 0); + m_segmentationMode = new QComboBox(w); + m_segmentationMode->setToolTip(ski->toolTip()); + m_segmentationMode->addItem(i18n("(default)"), QString()); + for (QMap::const_iterator it = vals.constBegin(); it != vals.constEnd(); ++it) + { + m_segmentationMode->addItem(it.value(), it.key()); + } + + if (vals.isEmpty()) m_segmentationMode->setEnabled(false); + else + { + int ix = m_segmentationMode->findData(KookaSettings::ocrTesseractSegmentationMode()); + if (ix!=-1) m_segmentationMode->setCurrentIndex(ix); + } + + gl->addWidget(m_segmentationMode, row, 1); + l->setBuddy(m_segmentationMode); + ++row; + + // OCR engine mode, auto detected values + vals = getValidValues("help-oem"); + ski = KookaSettings::self()->ocrTesseractEngineModeItem(); + Q_ASSERT(ski!=nullptr); + l = new QLabel(ski->label(), w); + gl->addWidget(l, row, 0); + m_engineMode = new QComboBox(w); + m_engineMode->setToolTip(ski->toolTip()); + m_engineMode->addItem(i18n("(default)"), QString()); + for (QMap::const_iterator it = vals.constBegin(); it != vals.constEnd(); ++it) + { + m_engineMode->addItem(it.value(), it.key()); + } + + if (vals.isEmpty()) m_engineMode->setEnabled(false); + else + { + int ix = m_engineMode->findData(KookaSettings::ocrTesseractEngineMode()); + if (ix!=-1) m_engineMode->setCurrentIndex(ix); + } + + gl->addWidget(m_engineMode, row, 1); + l->setBuddy(m_engineMode); + ++row; + + gl->setRowMinimumHeight(row, DialogBase::verticalSpacing()); + ++row; + + gl->setRowStretch(row-1, 1); // for top alignment + gl->setColumnStretch(1, 1); + + ocrShowInfo(m_ocrCmd, m_versionStr); // show the binary and version + progressBar()->setRange(0, 0); // progress animation only + + m_setupWidget = w; + return (!m_ocrCmd.isEmpty()); +} + + +void OcrTesseractDialog::slotWriteConfig() +{ + AbstractOcrDialogue::slotWriteConfig(); + + KookaSettings::setOcrTesseractBinary(getOCRCmd()); + + KookaSettings::setOcrTesseractLanguage(m_language->currentData().toString()); + KookaSettings::setOcrTesseractUserWords(m_userWords->url()); + KookaSettings::setOcrTesseractUserPatterns(m_userPatterns->url()); + KookaSettings::setOcrTesseractSegmentationMode(m_segmentationMode->currentData().toString()); + KookaSettings::setOcrTesseractEngineMode(m_engineMode->currentData().toString()); +} + + +void OcrTesseractDialog::enableFields(bool enable) +{ + m_setupWidget->setEnabled(enable); +} + + +void OcrTesseractDialog::getVersion(const QString &bin) +{ + qDebug() << "of" << bin; + if (bin.isEmpty()) return; + + KProcess proc; + proc.setOutputChannelMode(KProcess::MergedChannels); + proc << bin << "-v"; + + int status = proc.execute(5000); + if (status==0) + { + QByteArray output = proc.readAllStandardOutput(); + QRegExp rx("tesseract ([\\d\\.]+)"); + if (rx.indexIn(output)>-1) + { + m_ocrCmd = bin; + m_versionStr = rx.cap(1); + m_versionNum = m_versionStr.left(1).toInt(); + qDebug() << "version" << m_versionStr << "=" << m_versionNum; + } + } + else + { + qDebug() << "failed with status" << status; + m_versionStr = i18n("Error"); + } +} + + +QMap OcrTesseractDialog::getValidValues(const QString &opt) +{ + // The values displayed by and passed to Tesseract for OEM and PSM are integers, + // but the values for the language are strings. For simplicity we treat the + // OEM/PSM settings as strings also. + QMap result; + + KConfigSkeletonItem *ski = KookaSettings::self()->ocrTesseractValidValuesItem(); + Q_ASSERT(ski!=nullptr); + QString groupName = QString("%1_v%2").arg(ski->group()).arg(m_versionStr); + KConfigGroup grp = KookaSettings::self()->config()->group(groupName); + + if (grp.hasKey(opt+"_keys")) // values in config already + { + qDebug() << "option" << opt << "already in config"; + + const QStringList keys = grp.readEntry(opt+"_keys", QStringList()); + const QStringList descs = grp.readEntry(opt+"_descs", QStringList()); + for (int i = 0; i lines = output.split('\n'); + for (const QByteArray &line : lines) + { + const QString lineStr = QString::fromLocal8Bit(line); + qDebug() << "line:" << lineStr; + + QRegExp rx; + if (opt=="list-langs") rx.setPattern("^\\s*(\\w+)()$"); + else rx.setPattern("^\\s*(\\d+)\\s+(\\w.+)?$"); + if (rx.indexIn(lineStr)>-1) + { + const QString value = rx.cap(1); + QString desc = rx.cap(2).simplified(); + if (desc.endsWith(QLatin1Char('.')) || desc.endsWith(QLatin1Char(','))) desc.chop(1); + result.insert(value, desc); + } + } + + qDebug() << "parsed result count" << result.count(); + if (!result.isEmpty()) + { // save result for next time + grp.writeEntry(opt+"_keys", result.keys()); // ordered list of keys + grp.writeEntry(opt+"_descs", result.values()); // same-ordered list of values + grp.sync(); + } + } + + qDebug() << "values for" << opt << "=" << result.keys(); + return (result); +} diff --git a/plugins/ocr/gocr/ocrgocrengine.h b/plugins/ocr/tesseract/ocrtesseractdialog.h similarity index 60% copy from plugins/ocr/gocr/ocrgocrengine.h copy to plugins/ocr/tesseract/ocrtesseractdialog.h index f39c6af..25a6eb0 100644 --- a/plugins/ocr/gocr/ocrgocrengine.h +++ b/plugins/ocr/tesseract/ocrtesseractdialog.h @@ -1,69 +1,79 @@ /************************************************************************ * * * This file is part of Kooka, a scanning/OCR application using * * Qt and KDE Frameworks . * * * - * Copyright (C) 2000-2016 Klaas Freitag * - * Jonathan Marten * + * Copyright (C) 2020 Jonathan Marten * * * * Kooka is free software; you can redistribute it and/or modify it * * under the terms of the GNU Library General Public License as * * published by the Free Software Foundation and appearing in the * * file COPYING included in the packaging of this file; either * * version 2 of the License, or (at your option) any later version. * * * * As a special exception, permission is given to link this program * * with any version of the KADMOS OCR/ICR engine (a product of * * reRecognition GmbH, Kreuzlingen), and distribute the resulting * * executable without including the source code for KADMOS in the * * source distribution. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public * * License along with this program; see the file COPYING. If * * not, see . * * * ************************************************************************/ -#ifndef OCRGOCRENGINE_H -#define OCRGOCRENGINE_H +#ifndef OCRTESSERACTDIALOG_H +#define OCRTESSERACTDIALOG_H -#include +#include "abstractocrdialogue.h" -#include "abstractocrengine.h" -class QTemporaryDir; +class QWidget; +class QComboBox; +class KUrlRequester; -class OcrGocrEngine : public AbstractOcrEngine + +class OcrTesseractDialog : public AbstractOcrDialogue { Q_OBJECT public: - explicit OcrGocrEngine(QObject *pnt, const QVariantList &args); - ~OcrGocrEngine() override = default; + explicit OcrTesseractDialog(AbstractOcrEngine *plugin, QWidget *pnt); + ~OcrTesseractDialog() override = default; - AbstractOcrDialogue *createOcrDialogue(AbstractOcrEngine *plugin, QWidget *pnt) override; + bool setupGui() override; - bool hasAdvancedSettings() const override { return (true); } - void openAdvancedSettings() override; + QString getOCRCmd() const { return (m_ocrCmd); } + int getNumVersion() const { return (m_versionNum); } protected: - bool createOcrProcess(AbstractOcrDialogue *dia, const KookaImage *img) override; - QStringList tempFiles(bool retain) override; - bool finishedOcrProcess(QProcess *proc) override; + void enableFields(bool enable) override; protected slots: - void slotGOcrStdout(); + void slotWriteConfig() override; + +private: + void getVersion(const QString &bin); + QMap getValidValues(const QString &opt); private: - QTemporaryDir *m_tempDir; - QString m_inputFile; - QString m_resultFile; + QWidget *m_setupWidget; + QComboBox *m_engineMode; + QComboBox *m_segmentationMode; + QComboBox *m_language; + KUrlRequester *m_userWords; + KUrlRequester *m_userPatterns; + + QString m_ocrCmd; + int m_versionNum; + QString m_versionStr; }; -#endif // OCRGOCRENGINE_H +#endif // OCRTESSERACTDIALOG_H diff --git a/plugins/ocr/tesseract/ocrtesseractengine.cpp b/plugins/ocr/tesseract/ocrtesseractengine.cpp new file mode 100644 index 0000000..6f65eaa --- /dev/null +++ b/plugins/ocr/tesseract/ocrtesseractengine.cpp @@ -0,0 +1,255 @@ +/************************************************************************ + * * + * This file is part of Kooka, a scanning/OCR application using * + * Qt and KDE Frameworks . * + * * + * Copyright (C) 2020 Jonathan Marten * + * * + * Kooka is free software; you can redistribute it and/or modify it * + * under the terms of the GNU Library General Public License as * + * published by the Free Software Foundation and appearing in the * + * file COPYING included in the packaging of this file; either * + * version 2 of the License, or (at your option) any later version. * + * * + * As a special exception, permission is given to link this program * + * with any version of the KADMOS OCR/ICR engine (a product of * + * reRecognition GmbH, Kreuzlingen), and distribute the resulting * + * executable without including the source code for KADMOS in the * + * source distribution. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public * + * License along with this program; see the file COPYING. If * + * not, see . * + * * + ************************************************************************/ + +#include "ocrtesseractengine.h" + +#include +#include +#include +#include + +#include + +#include +#include + +#include "imageformat.h" +#include "kookasettings.h" +#include "ocrtesseractdialog.h" +#include "executablepathdialogue.h" + + +K_PLUGIN_FACTORY_WITH_JSON(OcrTesseractEngineFactory, "kookaocr-tesseract.json", registerPlugin();) +#include "ocrtesseractengine.moc" + + +OcrTesseractEngine::OcrTesseractEngine(QObject *pnt, const QVariantList &args) + : AbstractOcrEngine(pnt, "OcrTesseractEngine") +{ + m_ocrImageIn = QString(); + m_tempHOCROut = QString(); + m_tesseractVersion = 0; +} + + +AbstractOcrDialogue *OcrTesseractEngine::createOcrDialogue(AbstractOcrEngine *plugin, QWidget *pnt) +{ + return (new OcrTesseractDialog(plugin, pnt)); +} + + +bool OcrTesseractEngine::createOcrProcess(AbstractOcrDialogue *dia, const KookaImage *img) +{ + OcrTesseractDialog *parentDialog = static_cast(dia); + m_tesseractVersion = parentDialog->getNumVersion(); + + const QString cmd = parentDialog->getOCRCmd(); + + const QString ocrResultFile = tempSaveImage(img, ImageFormat("PGM"), 8); + setResultImage(ocrResultFile); + // TODO: if the input file is local and is readable by Tesseract, + // can use it directly (but don't delete it afterwards!) + m_ocrImageIn = tempSaveImage(img, ImageFormat("PNG"), 8); + + QProcess *proc = initOcrProcess(); // start process for OCR + QStringList args; // arguments for process + + // Input file + args << QFile::encodeName(m_ocrImageIn); // file with the input image + + // Output base name + m_tempHOCROut = tempFileName(""); // Tesseract just wants base name + args << QFile::encodeName(m_tempHOCROut); // the HOCR result file + m_tempHOCROut += ".hocr"; // suffix that it will have + + // Language + QString s = KookaSettings::ocrTesseractLanguage(); + if (!s.isEmpty()) args << "-l" << s; + + // User words + QUrl u = KookaSettings::ocrTesseractUserWords(); + if (u.isValid()) args << "--user-words" << u.toLocalFile(); + + // User patterns + u = KookaSettings::ocrTesseractUserPatterns(); + if (u.isValid()) args << "--user-patterns" << u.toLocalFile(); + + // Page segmentation mode + s = KookaSettings::ocrTesseractSegmentationMode(); + if (!s.isEmpty()) args << "--psm" << s; + + // OCR engine mode + s = KookaSettings::ocrTesseractEngineMode(); + if (!s.isEmpty()) args << "--oem" << s; + + //if (verboseDebug()) args << "-v"; + + s = KookaSettings::ocrTesseractExtraArguments(); + if (!s.isEmpty()) args << s; + + // Output format. This option generates HOCR (HTML with OCR markup) + // as specificied at http://kba.cloud/hocr-spec/1.2/ + args << "hocr"; + + proc->setProgram(cmd); + proc->setArguments(args); + + proc->setProcessChannelMode(QProcess::SeparateChannels); + m_tempStdoutLog = tempFileName("stdout.log"); + proc->setStandardOutputFile(m_tempStdoutLog); + + return (runOcrProcess()); +} + + +QStringList OcrTesseractEngine::tempFiles(bool retain) +{ + QStringList result; + result << m_ocrImageIn; + result << m_tempHOCROut; + result << m_tempStdoutLog; + return (result); +} + + +bool OcrTesseractEngine::finishedOcrProcess(QProcess *proc) +{ + qDebug(); + QString errStr = readHOCR(m_tempHOCROut); // parse the OCR results + if (errStr.isEmpty()) return (true); // parsed successfully + + setErrorText(errStr); // record the parse error + return (false); // parsing failed +} + + +QString OcrTesseractEngine::readHOCR(const QString &fileName) +{ + // some basic checks on the file + QFileInfo fi(fileName); + if (!fi.exists()) return (xi18nc("@info", "File %1 does not exist", fileName)); + if (!fi.isReadable()) return (xi18nc("@info", "File %1 unreadable", fileName)); + + qDebug() << "Starting to analyse HOCR" << fileName; + + QFile file(fileName); + if (!file.open(QIODevice::ReadOnly)) + { + return (xi18nc("@info", "Cannot open file %1", fileName)); + } + + startResultDocument(); // start document to receive results + + QXmlStreamReader reader(&file); + while (!reader.atEnd()) + { + reader.readNext(); // get next XML token + if (!reader.isStartElement()) continue; // only interested in element start + + // We only take note of elements defining new paragraphs, new lines and words. + // Examples of these are: + // + //

+ // + // The + // + // These same HTML elements may specify other page layout data which are + // not supported, but the class ID allows them to be distinguished. + + const QStringRef name = reader.name(); + if (name=="span" || name=="p") // may be either SPAN or P element + { + const QStringRef cls = reader.attributes().value("class"); + if (cls=="ocr_par" || cls=="ocr_line") // paragraph or line start + { + // The start of a paragraph is always followed by the start of a line. + // Generating a new output line for both means that paragraphs are + // separated by blank lines, as intended. + startLine(); + } + else if (cls=="ocrx_word") + { + OcrWordData wd; + + // The TITLE attribute of the SPAN element indicates the word bounding box. + const QString ttl = reader.attributes().value("title").toString(); + + QRegExp rx("bbox\\s+(\\d+)\\s+(\\d+)\\s+(\\d+)\\s+(\\d+);"); + if (!ttl.isEmpty() && ttl.contains(rx)) + { + QRect wordRect; + wordRect.setLeft(rx.cap(1).toInt()); + wordRect.setTop(rx.cap(2).toInt()); + wordRect.setRight(rx.cap(3).toInt()); + wordRect.setBottom(rx.cap(4).toInt()); + wd.setProperty(OcrWordData::Rectangle, wordRect); + } + + // The contained text is the recognised word. No child HTML elements + // are expected inside a word SPAN. + QString text = reader.readElementText(QXmlStreamReader::SkipChildElements); + addWord(text, wd); + } + } + } + + if (reader.hasError()) + { + qDebug() << "XML reader error, line" << reader.lineNumber() << reader.error(); + return (i18n("HOCR parsing error, %1", reader.errorString())); + } + + finishResultDocument(); // finished with output document + file.close(); // finished with HOCR file + + qDebug() << "Finished analysing HOCR"; + + return (QString()); // no error detected +} + + +void OcrTesseractEngine::openAdvancedSettings() +{ + ExecutablePathDialogue d(nullptr); + + QString exec = KookaSettings::ocrTesseractBinary(); + if (exec.isEmpty()) + { + KConfigSkeletonItem *ski = KookaSettings::self()->ocrTesseractBinaryItem(); + ski->setDefault(); + exec = KookaSettings::ocrTesseractBinary(); + } + + d.setPath(exec); + d.setLabel(i18n("Name or path of the Tesseract executable:")); + if (!d.exec()) return; + + KookaSettings::setOcrTesseractBinary(d.path()); +} diff --git a/plugins/ocr/gocr/ocrgocrengine.h b/plugins/ocr/tesseract/ocrtesseractengine.h similarity index 75% copy from plugins/ocr/gocr/ocrgocrengine.h copy to plugins/ocr/tesseract/ocrtesseractengine.h index f39c6af..547d362 100644 --- a/plugins/ocr/gocr/ocrgocrengine.h +++ b/plugins/ocr/tesseract/ocrtesseractengine.h @@ -1,69 +1,66 @@ /************************************************************************ * * * This file is part of Kooka, a scanning/OCR application using * * Qt and KDE Frameworks . * * * - * Copyright (C) 2000-2016 Klaas Freitag * - * Jonathan Marten * + * Copyright (C) 2020 Jonathan Marten * * * * Kooka is free software; you can redistribute it and/or modify it * * under the terms of the GNU Library General Public License as * * published by the Free Software Foundation and appearing in the * * file COPYING included in the packaging of this file; either * * version 2 of the License, or (at your option) any later version. * * * * As a special exception, permission is given to link this program * * with any version of the KADMOS OCR/ICR engine (a product of * * reRecognition GmbH, Kreuzlingen), and distribute the resulting * * executable without including the source code for KADMOS in the * * source distribution. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public * * License along with this program; see the file COPYING. If * * not, see . * * * ************************************************************************/ -#ifndef OCRGOCRENGINE_H -#define OCRGOCRENGINE_H - -#include +#ifndef OCRTESSERACTENGINE_H +#define OCRTESSERACTENGINE_H #include "abstractocrengine.h" -class QTemporaryDir; - -class OcrGocrEngine : public AbstractOcrEngine +class OcrTesseractEngine : public AbstractOcrEngine { Q_OBJECT public: - explicit OcrGocrEngine(QObject *pnt, const QVariantList &args); - ~OcrGocrEngine() override = default; + explicit OcrTesseractEngine(QObject *pnt, const QVariantList &args); + ~OcrTesseractEngine() override = default; AbstractOcrDialogue *createOcrDialogue(AbstractOcrEngine *plugin, QWidget *pnt) override; - bool hasAdvancedSettings() const override { return (true); } + bool hasAdvancedSettings() const override { return (true); } void openAdvancedSettings() override; protected: bool createOcrProcess(AbstractOcrDialogue *dia, const KookaImage *img) override; QStringList tempFiles(bool retain) override; bool finishedOcrProcess(QProcess *proc) override; -protected slots: - void slotGOcrStdout(); +private: + QString readHOCR(const QString &fileName); private: - QTemporaryDir *m_tempDir; - QString m_inputFile; - QString m_resultFile; + QString m_ocrImageIn; + QString m_tempHOCROut; + QString m_tempStdoutLog; + + int m_tesseractVersion; }; -#endif // OCRGOCRENGINE_H +#endif // OCRTESSERACTENGINE_H diff --git a/plugins/ocr/tesseract/tesseract.png b/plugins/ocr/tesseract/tesseract.png new file mode 100644 index 0000000..008c814 Binary files /dev/null and b/plugins/ocr/tesseract/tesseract.png differ