diff --git a/app/kookaview.cpp b/app/kookaview.cpp index 6a006b5..190a57a 100644 --- a/app/kookaview.cpp +++ b/app/kookaview.cpp @@ -1,1255 +1,1255 @@ /************************************************************************ * * * This file is part of Kooka, a scanning/OCR application using * * Qt and KDE Frameworks . * * * * Copyright (C) 1999-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 "kookaview.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "scandevices.h" #include "kscandevice.h" #include "imagemetainfo.h" #include "deviceselector.h" #include "adddevicedialog.h" #include "previewer.h" #include "imagecanvas.h" #include "kookapref.h" #include "galleryhistory.h" #include "thumbview.h" #include "kookaimage.h" #include "abstractocrengine.h" #include "pluginmanager.h" #include "ocrresedit.h" #include "scanparamsdialog.h" #include "kookagallery.h" #include "kookascanparams.h" #include "scangallery.h" #include "imagetransform.h" #include "statusbarmanager.h" #include "kookasettings.h" #include "imgprintdialog.h" #include "kookaprint.h" #ifndef KDE4 #include "photocopyprintdialogpage.h" #endif // --------------------------------------------------------------------------- // Some of the UI panels (the gallery and the image viewer) are common // to more that one of the main task tabs. This means that they can't simply // be added to the tabs/splitters in the normal way, as a widget can only be // a child of one parent at a time. // // This WidgetSite acts as a layout placeholder for such reassignable widgets. // It is assigned a new child widget when tabs are switched. // // It also defines the frame style for the panels. So, in order to maintain a // consistent look, all of those panels should derive from QWidget (or, if // from QFrame, do not set a frame style) and should set the margin of any // internal layout to 0. (KHBox/KVBox do this automatically, but any other // layout needs the margin explicitly set). class WidgetSite : public QFrame { public: WidgetSite(QWidget *parent, QWidget *widget = nullptr); void setWidget(QWidget *widget); private: static int sCount; }; int WidgetSite::sCount = 0; WidgetSite::WidgetSite(QWidget *parent, QWidget *widget) : QFrame(parent) { QString name = QString("WidgetSite-#%1").arg(++sCount); setObjectName(name.toLocal8Bit()); setFrameStyle(QFrame::Panel | QFrame::Raised); // from "scanparams.cpp" setLineWidth(1); QGridLayout *lay = new QGridLayout(this); lay->setRowStretch(0, 1); lay->setColumnStretch(0, 1); if (widget == nullptr) { QLabel *l = new QLabel(name, this); l->setAlignment(Qt::AlignVCenter | Qt::AlignHCenter); widget = l; } ////qDebug() << name // << "widget is a" << widget->metaObject()->className() // << "parent is a" << widget->parent()->metaObject()->className(); lay->addWidget(widget, 0, 0); widget->show(); } void WidgetSite::setWidget(QWidget *widget) { QGridLayout *lay = static_cast(layout()); QObjectList childs = children(); for (QObjectList::iterator it = childs.begin(); it != childs.end(); ++it) { QObject *ch = (*it); if (ch->isWidgetType()) { QWidget *w = static_cast(ch); w->hide(); lay->removeWidget(w); } } ////qDebug() << objectName() // << "widget is a" << widget->metaObject()->className() // << "parent is a" << widget->parent()->metaObject()->className(); lay->addWidget(widget, 0, 0); widget->show(); } // --------------------------------------------------------------------------- // Convenience class for layout splitter. class WidgetSplitter : public QSplitter { public: WidgetSplitter(Qt::Orientation orientation, QWidget *parent = nullptr); }; WidgetSplitter::WidgetSplitter(Qt::Orientation orientation, QWidget *parent) : QSplitter(orientation, parent) { setChildrenCollapsible(false); setContentsMargins(0, 0, 0, 0); setStretchFactor(1, 1); } // --------------------------------------------------------------------------- KookaView::KookaView(KMainWindow *parent, const QByteArray &deviceToUse) : QTabWidget(parent) { setObjectName("KookaView"); mMainWindow = parent; mOcrResultImg = nullptr; mScanParams = nullptr; // mOcrEngine = nullptr; mCurrentTab = KookaView::TabNone; mIsPhotoCopyMode = false; /** Image Viewer **/ mImageCanvas = new ImageCanvas(this); mImageCanvas->setMinimumSize(100, 200); QMenu *ctxtmenu = mImageCanvas->contextMenu(); if (ctxtmenu != nullptr) { ctxtmenu->addSection(i18n("Image View")); } // Connections ImageCanvas --> myself connect(mImageCanvas, SIGNAL(newRect(QRect)), SLOT(slotSelectionChanged(QRect))); /** Thumbnail View **/ mThumbView = new ThumbView(this); /** Scan Gallery **/ mGallery = new KookaGallery(this); ScanGallery *packager = mGallery->galleryTree(); // Connections ScanGallery --> myself connect(packager, SIGNAL(itemHighlighted(QUrl,bool)), SLOT(slotGallerySelectionChanged())); connect(packager, SIGNAL(showImage(const KookaImage*,bool)), SLOT(slotShowAImage(const KookaImage*,bool))); connect(packager, SIGNAL(aboutToShowImage(QUrl)), SLOT(slotStartLoading(QUrl))); connect(packager, SIGNAL(unloadImage(const KookaImage*)), SLOT(slotUnloadAImage(const KookaImage*))); // Connections ScanGallery --> ThumbView connect(packager, SIGNAL(itemHighlighted(QUrl,bool)), mThumbView, SLOT(slotHighlightItem(QUrl,bool))); connect(packager, SIGNAL(imageChanged(const KFileItem*)), mThumbView, SLOT(slotImageChanged(const KFileItem*))); connect(packager, SIGNAL(fileRenamed(const KFileItem*,QString)), mThumbView, SLOT(slotImageRenamed(const KFileItem*,QString))); // Connections ThumbView --> ScanGallery connect(mThumbView, SIGNAL(itemHighlighted(QUrl)), packager, SLOT(slotHighlightItem(QUrl))); connect(mThumbView, SIGNAL(itemActivated(QUrl)), packager, SLOT(slotActivateItem(QUrl))); GalleryHistory *recentFolder = mGallery->galleryRecent(); // Connections ScanGallery <--> recent folder history connect(packager, SIGNAL(galleryPathChanged(const FileTreeBranch*,QString)), recentFolder, SLOT(slotPathChanged(const FileTreeBranch*,QString))); connect(packager, SIGNAL(galleryDirectoryRemoved(const FileTreeBranch*,QString)), recentFolder, SLOT(slotPathRemoved(const FileTreeBranch*,QString))); connect(recentFolder, SIGNAL(pathSelected(QString,QString)), packager, SLOT(slotSelectDirectory(QString,QString))); /** Scanner Settings **/ if (!deviceToUse.isEmpty() && deviceToUse != "gallery") { ScanDevices::self()->addUserSpecifiedDevice(deviceToUse, "on command line", "", true); } /** Scanner Device **/ mScanDevice = new KScanDevice(this); // Connections KScanDevice --> myself connect(mScanDevice, SIGNAL(sigScanFinished(KScanDevice::Status)), SLOT(slotScanFinished(KScanDevice::Status))); connect(mScanDevice, SIGNAL(sigScanFinished(KScanDevice::Status)), SLOT(slotPhotoCopyScan(KScanDevice::Status))); /* New image created after scanning now call save dialog*/ connect(mScanDevice, SIGNAL(sigNewImage(const QImage*,const ImageMetaInfo*)), SLOT(slotNewImageScanned(const QImage*,const ImageMetaInfo*))); /* New image created after scanning now print it*/ connect(mScanDevice, SIGNAL(sigNewImage(const QImage*,const ImageMetaInfo*)), SLOT(slotPhotoCopyPrint(const QImage*,const ImageMetaInfo*))); /* New preview image */ connect(mScanDevice, SIGNAL(sigNewPreview(const QImage*,const ImageMetaInfo*)), SLOT(slotNewPreview(const QImage*,const ImageMetaInfo*))); connect(mScanDevice, SIGNAL(sigScanStart(const ImageMetaInfo*)), SLOT(slotScanStart(const ImageMetaInfo*))); connect(mScanDevice, SIGNAL(sigAcquireStart()), SLOT(slotAcquireStart())); /** Scan Preview **/ mPreviewCanvas = new Previewer(this); mPreviewCanvas->setMinimumSize(100, 100); ctxtmenu = mPreviewCanvas->getImageCanvas()->contextMenu(); if (ctxtmenu != nullptr) { ctxtmenu->addSection(i18n("Scan Preview")); } // Connections Previewer --> myself connect(mPreviewCanvas, SIGNAL(previewDimsChanged(QString)), SLOT(slotPreviewDimsChanged(QString))); /** Ocr Result Text **/ mOcrResEdit = new OcrResEdit(this); mOcrResEdit->setAcceptRichText(false); mOcrResEdit->setWordWrapMode(QTextOption::NoWrap); mOcrResEdit->setPlaceholderText(i18n("OCR results will appear here")); connect(mOcrResEdit, SIGNAL(spellCheckStatus(QString)), this, SIGNAL(changeStatus(QString))); /** Tabs **/ // TODO: not sure which tab position is best, make this configurable setTabPosition(QTabWidget::West); setTabsClosable(false); mScanPage = new WidgetSplitter(Qt::Horizontal, this); addTab(mScanPage, QIcon::fromTheme("scan"), i18n("Scan")); mGalleryPage = new WidgetSplitter(Qt::Horizontal, this); addTab(mGalleryPage, QIcon::fromTheme("image-x-generic"), i18n("Gallery")); mOcrPage = new WidgetSplitter(Qt::Vertical, this); addTab(mOcrPage, QIcon::fromTheme("ocr"), i18n("OCR")); connect(this, SIGNAL(currentChanged(int)), SLOT(slotTabChanged(int))); // TODO: make the splitter layouts and sizes configurable mParamsSite = new WidgetSite(this); mScanGallerySite = new WidgetSite(this); mGalleryGallerySite = new WidgetSite(this); mOcrGallerySite = new WidgetSite(this); mGalleryImgviewSite = new WidgetSite(this); mOcrImgviewSite = new WidgetSite(this); // Even widgets that are not shared between tabs are placed in a WidgetSite, // to keep a consistent frame appearance. // "Scan" page: gallery top left, scan parameters bottom left, preview right mScanSubSplitter = new WidgetSplitter(Qt::Vertical, mScanPage); mScanSubSplitter->addWidget(mScanGallerySite); // TL mScanSubSplitter->addWidget(mParamsSite); // BL mScanPage->addWidget(new WidgetSite(this, mPreviewCanvas)); // R // "Gallery" page: gallery left, viewer top right, thumbnails bottom right mGalleryPage->addWidget(mGalleryGallerySite); // L mGallerySubSplitter = new WidgetSplitter(Qt::Vertical, mGalleryPage); mGallerySubSplitter->addWidget(mGalleryImgviewSite); // TR mGallerySubSplitter->addWidget(new WidgetSite(this, mThumbView)); // BR // "OCR" page: gallery top left, viewer top right, results bottom mOcrSubSplitter = new WidgetSplitter(Qt::Horizontal, mOcrPage); mOcrSubSplitter->addWidget(mOcrGallerySite); // TL mOcrSubSplitter->addWidget(mOcrImgviewSite); // TR mOcrPage->addWidget(new WidgetSite(this, mOcrResEdit)); // B if (slotSelectDevice(deviceToUse, false)) { slotTabChanged(KookaView::TabScan); } else { setCurrentIndex(KookaView::TabGallery); } } KookaView::~KookaView() { delete mScanDevice; //qDebug(); } // this gets called via Kooka::closeEvent() at shutdown void KookaView::saveWindowSettings(KConfigGroup &grp) { qDebug() << "to group" << grp.name(); KookaSettings::setLayoutTabIndex(currentIndex()); KookaSettings::setLayoutScan1(mScanPage->saveState().toBase64()); KookaSettings::setLayoutScan2(mScanSubSplitter->saveState().toBase64()); KookaSettings::setLayoutGallery1(mGalleryPage->saveState().toBase64()); KookaSettings::setLayoutGallery2(mGallerySubSplitter->saveState().toBase64()); KookaSettings::setLayoutOcr1(mOcrPage->saveState().toBase64()); KookaSettings::setLayoutOcr2(mOcrSubSplitter->saveState().toBase64()); saveGalleryState(); // for the current tab KookaSettings::self()->save(); } // this gets called by Kooka::applyMainWindowSettings() at startup void KookaView::applyWindowSettings(const KConfigGroup &grp) { qDebug() << "from group" << grp.name(); QString set = KookaSettings::layoutScan1(); if (!set.isEmpty()) mScanPage->restoreState(QByteArray::fromBase64(set.toLocal8Bit())); set = KookaSettings::layoutScan2(); if (!set.isEmpty()) mScanSubSplitter->restoreState(QByteArray::fromBase64(set.toLocal8Bit())); set = KookaSettings::layoutGallery1(); if (!set.isEmpty()) mGalleryPage->restoreState(QByteArray::fromBase64(set.toLocal8Bit())); set = KookaSettings::layoutGallery2(); if (!set.isEmpty()) mGallerySubSplitter->restoreState(QByteArray::fromBase64(set.toLocal8Bit())); set = KookaSettings::layoutOcr1(); if (!set.isEmpty()) mOcrPage->restoreState(QByteArray::fromBase64(set.toLocal8Bit())); set = KookaSettings::layoutOcr2(); if (!set.isEmpty()) mOcrSubSplitter->restoreState(QByteArray::fromBase64(set.toLocal8Bit())); } void KookaView::saveGalleryState(int index) const { if (index == -1) index = currentIndex(); gallery()->saveHeaderState(index); } void KookaView::restoreGalleryState(int index) { if (index == -1) index = currentIndex(); gallery()->restoreHeaderState(index); } void KookaView::slotTabChanged(int index) { //qDebug() << index; if (mCurrentTab != KookaView::TabNone) { saveGalleryState(mCurrentTab); } // save state of previous tab switch (index) { case KookaView::TabScan: // Scan mScanGallerySite->setWidget(mGallery); mGalleryImgviewSite->setWidget(mImageCanvas); // somewhere to park it emit clearStatus(StatusBarManager::ImageDims); break; case KookaView::TabGallery: // Gallery mGalleryGallerySite->setWidget(mGallery); mGalleryImgviewSite->setWidget(mImageCanvas); emit clearStatus(StatusBarManager::PreviewDims); break; case KookaView::TabOcr: // OCR mOcrGallerySite->setWidget(mGallery); mOcrImgviewSite->setWidget(mImageCanvas); emit clearStatus(StatusBarManager::PreviewDims); break; } restoreGalleryState(index); // restore state of new tab mCurrentTab = static_cast(index); // note for next tab change updateSelectionState(); // update image action states } bool KookaView::slotSelectDevice(const QByteArray &useDevice, bool alwaysAsk) { //qDebug() << "use device" << useDevice << "ask" << alwaysAsk; bool haveConnection = false; bool gallery_mode = (useDevice == "gallery"); QByteArray selDevice; /* in case useDevice is the term 'gallery', the user does not want to * connect to a scanner, but only work in gallery mode. Otherwise, try * to read the device to use from config or from a user dialog */ if (!gallery_mode) { selDevice = useDevice; if (selDevice.isEmpty()) { selDevice = userDeviceSelection(alwaysAsk); } if (selDevice.isEmpty()) { // dialogue cancelled if (mScanParams != nullptr) { return (false); // have setup, do nothing } gallery_mode = true; } } if (mScanParams != nullptr) { closeScanDevice(); // remove existing GUI object } mScanParams = new KookaScanParams(this); // and create a new one connect(mScanParams, SIGNAL(actionSelectScanner()), SLOT(slotSelectDevice())); connect(mScanParams, SIGNAL(actionAddScanner()), SLOT(slotAddDevice())); mParamsSite->setWidget(mScanParams); if (!selDevice.isEmpty()) { // connect to the selected scanner while (!haveConnection) { //qDebug() << "Opening device" << selDevice; KScanDevice::Status stat = mScanDevice->openDevice(selDevice); if (stat == KScanDevice::Ok) { haveConnection = true; } else { QString msg = xi18nc("@info", "There was a problem opening the scanner device." "" "Check that the scanner is connected and switched on, " "and that SANE support for it is correctly configured." "" "Trying to use scanner device: %3" "libkookascan reported error: %2" "SANE reported error: %1", mScanDevice->lastSaneErrorMessage(), KScanDevice::statusMessage(stat), selDevice.constData()); int tryAgain = KMessageBox::warningContinueCancel(mMainWindow, msg, QString(), KGuiItem("Retry")); if (tryAgain == KMessageBox::Cancel) { break; } } } if (haveConnection) { // scanner connected OK QSize s = mScanDevice->getMaxScanSize(); // fix for 160148 //qDebug() << "scanner max size" << s; mPreviewCanvas->setScannerBedSize(s.width(), s.height()); // Connections ScanParams --> Previewer connect(mScanParams, SIGNAL(scanResolutionChanged(int,int)), mPreviewCanvas, SLOT(slotNewScanResolutions(int,int))); connect(mScanParams, SIGNAL(scanModeChanged(int)), mPreviewCanvas, SLOT(slotNewScanMode(int))); connect(mScanParams, SIGNAL(newCustomScanSize(QRect)), mPreviewCanvas, SLOT(slotNewCustomScanSize(QRect))); // Connections Previewer --> ScanParams connect(mPreviewCanvas, SIGNAL(newPreviewRect(QRect)), mScanParams, SLOT(slotNewPreviewRect(QRect))); mScanParams->connectDevice(mScanDevice); // create scanning options mPreviewCanvas->setPreviewImage(mScanDevice->loadPreviewImage()); // load saved preview image mPreviewCanvas->connectScanner(mScanDevice); // load its autosel options } } if (!haveConnection) { // no scanner device available, // or starting in gallery mode if (mScanParams != nullptr) { mScanParams->connectDevice(nullptr, gallery_mode); } } emit signalScannerChanged(haveConnection); return (haveConnection); } void KookaView::slotAddDevice() { AddDeviceDialog d(mMainWindow, i18n("Add Scan Device")); if (d.exec()) { QByteArray dev = d.getDevice(); QString dsc = d.getDescription(); QByteArray type = d.getType(); //qDebug() << "dev" << dev << "type" << type << "desc" << dsc; ScanDevices::self()->addUserSpecifiedDevice(dev, dsc, type); } } QByteArray KookaView::userDeviceSelection(bool alwaysAsk) { /* a list of backends the scan backend knows */ QList backends = ScanDevices::self()->allDevices(); if (backends.count() == 0) { if (KMessageBox::warningContinueCancel(mMainWindow, xi18nc("@info", "No scanner devices are available." "" "If your scanner is a type that can be auto-detected by SANE, " "check that it is connected, switched on and configured correctly." "" "If the scanner cannot be auto-detected by SANE (this includes some network scanners), " "you need to specify the device to use. " "Use the Add Scan Device option to enter the backend name and parameters, " "or see that dialog for more information."), QString(), KGuiItem(i18n("Add Scan Device..."))) != KMessageBox::Continue) { return (""); } slotAddDevice(); backends = ScanDevices::self()->allDevices(); // refresh the list if (backends.count() == 0) { return (""); // give up this time } } QByteArray selDevice; DeviceSelector ds(mMainWindow, backends, (alwaysAsk ? KGuiItem() : KGuiItem(i18n("Gallery"), QIcon::fromTheme("image-x-generic")))); if (!alwaysAsk) { selDevice = ds.getDeviceFromConfig(); } if (selDevice.isEmpty()) { //qDebug() << "no selDevice, starting selector"; if (ds.exec() == QDialog::Accepted) { selDevice = ds.getSelectedDevice(); } //qDebug() << "selector returned device" << selDevice; } return (selDevice); } QString KookaView::scannerName() const { if (mScanDevice->scannerBackendName().isEmpty()) { return (i18n("Gallery")); } if (!mScanDevice->isScannerConnected()) { return (i18n("No scanner connected")); } return (mScanDevice->scannerDescription()); } bool KookaView::isScannerConnected() const { return (mScanDevice->isScannerConnected()); } void KookaView::slotSelectionChanged(const QRect &newSelection) { emit signalRectangleChanged(newSelection.isValid()); } // The 'state' generated here is used by Kooka::slotUpdateViewActions(). void KookaView::updateSelectionState() { KookaView::StateFlags state = KookaView::StateFlags(); const FileTreeViewItem *ftvi = mGallery->galleryTree()->highlightedFileTreeViewItem(); const KFileItem *fi = (ftvi != nullptr ? ftvi->fileItem() : nullptr); const bool scanmode = (mCurrentTab == KookaView::TabScan); if (scanmode) // Scan mode { if (mPreviewCanvas->getImageCanvas()->hasImage()) state |= KookaView::PreviewValid; } else // Gallery/OCR mode { state |= KookaView::GalleryShown; } if (fi != nullptr && !fi->isNull()) // have a gallery selection { if (fi->isDir()) state |= KookaView::IsDirectory; else { state |= KookaView::FileSelected; if (fi->url().hasFragment()) state |= KookaView::IsSubImage; if (ftvi->childCount()>0) state |= KookaView::HasSubImages; } } if (ftvi != nullptr && ftvi->isRoot()) state |= KookaView::RootSelected; if (imageViewer()->hasImage()) state |= KookaView::ImageValid; ////qDebug() << "state" << state; emit signalViewSelectionState(state); } void KookaView::slotGallerySelectionChanged() { const FileTreeViewItem *ftvi = mGallery->galleryTree()->highlightedFileTreeViewItem(); const KFileItem *fi = (ftvi != nullptr ? ftvi->fileItem() : nullptr); const bool scanmode = (mCurrentTab == KookaView::TabScan); if (fi == nullptr || fi->isNull()) { // no gallery selection if (!scanmode) { emit changeStatus(i18n("No selection")); } } else { // have a gallery selection if (!scanmode) { KLocalizedString str = fi->isDir() ? ki18n("Gallery folder %1") : ki18n("Gallery image %1"); emit changeStatus(str.subs(fi->url().url(QUrl::PreferLocalFile)).toString()); } } updateSelectionState(); } void KookaView::loadStartupImage() { const bool wantReadOnStart = KookaSettings::startupReadImage(); if (wantReadOnStart) { QString startup = KookaSettings::startupSelectedImage(); //qDebug() << "load startup image" << startup; if (!startup.isEmpty()) gallery()->slotSelectImage(QUrl::fromLocalFile(startup)); } else { //qDebug() << "do not load startup image"; } } void KookaView::print() { const KookaImage *img = gallery()->getCurrImage(true); if (img==nullptr) return; // load image if necessary // create a KookaPrint (subclass of a QPrinter) KookaPrint printer; printer.setImage(img); QPrintDialog d(&printer, this); d.setWindowTitle(i18nc("@title:window", "Print Image")); d.setOptions(QAbstractPrintDialog::PrintToFile|QAbstractPrintDialog::PrintShowPageSize); // TODO (investigate): even with the options set as above, the options below still // appear in the print dialogue. Is this as intended by Qt? // d.setOption(QAbstractPrintDialog::PrintSelection, false); // d.setOption(QAbstractPrintDialog::PrintPageRange, false); ImgPrintDialog imgTab(img, &printer); // create tab for our options d.setOptionTabs(QList() << &imgTab); // add tab to print dialogue if (!d.exec()) return; // open the dialogue QString msg = imgTab.checkValid(); // check that settings are valid if (!msg.isEmpty()) // if not, display error message { KMessageBox::sorry(this, i18nc("@info", "Invalid print options were specified:\n\n%1", msg), i18nc("@title:window", "Cannot Print")); return; } imgTab.updatePrintParameters(); // set final printer options printer.printImage(); // print the image } void KookaView::slotNewPreview(const QImage *newimg, const ImageMetaInfo *info) { if (newimg == nullptr) { return; } //qDebug() << "new preview image, size" << newimg->size() //<< "res [" << info->getXResolution() << "x" << info->getYResolution() << "]"; mPreviewCanvas->newImage(newimg); // set new image and size updateSelectionState(); } void KookaView::slotStartOcrSelection() { emit changeStatus(i18n("Starting OCR on selection")); startOCR(KookaImage(mImageCanvas->selectedImage())); emit clearStatus(); } void KookaView::slotStartOcr() { emit changeStatus(i18n("Starting OCR on the image")); startOCR(*gallery()->getCurrImage(true)); emit clearStatus(); } void KookaView::slotSetOcrSpellConfig(const QString &configFile) { #ifndef KF5 //qDebug() << configFile; if (mOcrResEdit!=nullptr) mOcrResEdit->setSpellCheckingConfigFileName(configFile); #endif } void KookaView::slotOcrSpellCheck(bool interactive, bool background) { if (!interactive && !background) // not doing anything { emit changeStatus(i18n("OCR finished")); return; } if (mOcrResEdit->document()->isEmpty()) { KMessageBox::sorry(mMainWindow, i18n("There is no OCR result text to spell check."), i18n("OCR Spell Check not possible")); return; } setCurrentIndex(KookaView::TabOcr); emit changeStatus(i18n("OCR Spell Check")); if (background) mOcrResEdit->setCheckSpellingEnabled(true); if (interactive) mOcrResEdit->checkSpelling(); } -void KookaView::startOCR(const KookaImage img) +void KookaView::startOCR(const KookaImage &img) { if (img.isNull()) return; // no image to OCR setCurrentIndex(KookaView::TabOcr); const QString engineName = KookaSettings::ocrEngineName(); if (engineName.isEmpty()) { int result = KMessageBox::warningContinueCancel(mMainWindow, i18n("No OCR engine is configured.\n" "Please select and configure one in order to perform OCR."), i18n("OCR Not Configured"), KGuiItem(i18n("Configure OCR..."))); if (result==KMessageBox::Continue) emit signalOcrPrefs(); return; } AbstractOcrEngine *engine = qobject_cast(PluginManager::self()->loadPlugin(PluginManager::OcrPlugin, engineName)); if (engine==nullptr) { KMessageBox::error(mMainWindow, i18n("Cannot load OCR plugin '%1'", engineName)); return; } // We don't know whether the plugin object has been used before. // So disconnect all of its existing signals so that they do not // get double connected. engine->disconnect(); // Connections OCR Engine --> myself connect(engine, &AbstractOcrEngine::newOCRResultText, this, &KookaView::slotOcrResultAvailable); connect(engine, &AbstractOcrEngine::setSpellCheckConfig, this, &KookaView::slotSetOcrSpellConfig); connect(engine, &AbstractOcrEngine::startSpellCheck, this, &KookaView::slotOcrSpellCheck); connect(engine, &AbstractOcrEngine::openOcrPrefs, this, &KookaView::signalOcrPrefs); // Connections OCR Results --> OCR Engine connect(mOcrResEdit, &OcrResEdit::highlightWord, engine, &AbstractOcrEngine::slotHighlightWord); connect(mOcrResEdit, &OcrResEdit::scrollToWord, engine, &AbstractOcrEngine::slotScrollToWord); // Connections OCR Engine --> OCR Results connect(engine, &AbstractOcrEngine::readOnlyEditor, mOcrResEdit, &OcrResEdit::slotSetReadOnly); connect(engine, &AbstractOcrEngine::selectWord, mOcrResEdit, &OcrResEdit::slotSelectWord); engine->setImageCanvas(mImageCanvas); engine->setTextDocument(mOcrResEdit->document()); engine->setImage(img); engine->openOcrDialogue(this); } void KookaView::slotOcrResultAvailable() { emit signalOcrResultAvailable(mOcrResEdit->document()->characterCount()>0); } void KookaView::slotScanStart(const ImageMetaInfo *info) { //qDebug() << "Scan starts..."; if (KookaSettings::saverAskBeforeScan()) // ask for filename first? { if (info != nullptr) // if we have initial image info { //qDebug() << "imgtype" << info->getImageType(); if (!gallery()->prepareToSave(info)) // get ready to save { // user cancelled file prompt mScanDevice->slotStopScanning(); // abort the scan now return; } } } if (mScanParams != nullptr) { mScanParams->setEnabled(false); KLed *led = mScanParams->operationLED(); if (led != nullptr) { led->setColor(Qt::red); // scanner warming up led->setState(KLed::On); qApp->processEvents(); // let the change show } mScanParams->setScanDestination(gallery()->saveURL().url(QUrl::PreferLocalFile)); } } void KookaView::slotAcquireStart() { //qDebug() << "Acquire starts"; if (mScanParams != nullptr) { KLed *led = mScanParams->operationLED(); if (led != nullptr) { led->setColor(Qt::green); // scanning active qApp->processEvents(); // let the change show } } } void KookaView::slotNewImageScanned(const QImage *img, const ImageMetaInfo *info) { if (mIsPhotoCopyMode) { return; } gallery()->addImage(img, info); } void KookaView::slotScanFinished(KScanDevice::Status stat) { //qDebug() << "Scan finished with status" << stat; if (stat != KScanDevice::Ok && stat != KScanDevice::Cancelled) { QString msg = xi18nc("@info", "There was a problem during preview or scanning." "" "Check that the scanner is still connected and switched on, " "" "and that media is loaded if required." "" "Trying to use scanner device: %3" "libkookascan reported error: %2" "SANE reported error: %1", mScanDevice->lastSaneErrorMessage(), KScanDevice::statusMessage(stat), mScanDevice->scannerBackendName().constData()); KMessageBox::error(mMainWindow, msg); } if (mScanParams != nullptr) { mScanParams->setEnabled(true); KLed *led = mScanParams->operationLED(); if (led != nullptr) { led->setColor(Qt::green); led->setState(KLed::Off); } } } void KookaView::closeScanDevice() { //qDebug() << "Scanner Device closes down"; if (mScanParams != nullptr) { delete mScanParams; mScanParams = nullptr; } mScanDevice->closeDevice(); } void KookaView::slotCreateNewImgFromSelection() { emit changeStatus(i18n("Create new image from selection")); QImage img = mImageCanvas->selectedImage(); if (!img.isNull()) { gallery()->addImage(&img); } emit clearStatus(); } void KookaView::slotTransformImage() { if (imageViewer()->isReadOnly()) { emit changeStatus(i18n("Cannot transform, image is read-only")); return; } // Get operation code from action that was triggered QAction *act = static_cast(sender()); if (act == nullptr) { return; } ImageTransform::Operation op = static_cast(act->data().toInt()); // This may appear to be a waste, since we are immediately unloading the image. // But we need a copy of the image anyway, so there will still be a load needed. const KookaImage *loadedImage = gallery()->getCurrImage(true); if (loadedImage == nullptr) { return; } const QImage img = *loadedImage; // get a copy of the image QString imageFile = gallery()->currentImageFileName(); if (imageFile.isEmpty()) { return; // get file to save back to } gallery()->slotUnloadItems(); // unload original from memory QThread *transformer = new ImageTransform(img, op, imageFile, this); connect(transformer, SIGNAL(done(QUrl)), gallery(), SLOT(slotUpdatedItem(QUrl))); connect(transformer, SIGNAL(statusMessage(QString)), SIGNAL(changeStatus(QString))); connect(transformer, SIGNAL(finished()), transformer, SLOT(deleteLater())); transformer->start(); } void KookaView::slotSaveOcrResult() { if (mOcrResEdit != nullptr) { mOcrResEdit->slotSaveText(); } } void KookaView::slotScanParams() { if (mScanDevice == nullptr) { return; // must have a scanner device } ScanParamsDialog d(this, mScanDevice); d.exec(); } void KookaView::slotShowAImage(const KookaImage *img, bool isDir) { if (mImageCanvas != nullptr) { // load into image viewer mImageCanvas->newImage(img); mImageCanvas->setReadOnly(false); } if (mImageCanvas != nullptr) { emit changeStatus(mImageCanvas->imageInfoString(), StatusBarManager::ImageDims); } if (img != nullptr) { emit changeStatus(i18n("Loaded image %1", img->url().url(QUrl::PreferLocalFile))); } else if (!isDir) { emit changeStatus(i18n("Unloaded image")); } updateSelectionState(); } void KookaView::slotUnloadAImage(const KookaImage *img) { //qDebug() << "Unloading Image"; if (mImageCanvas != nullptr) { mImageCanvas->newImage(nullptr); } updateSelectionState(); } /* this slot is called when the user clicks on an image in the packager * and loading of the image starts */ void KookaView::slotStartLoading(const QUrl &url) { emit changeStatus(i18n("Loading %1...", url.url(QUrl::PreferLocalFile))); } void KookaView::connectViewerAction(QAction *action, bool sepBefore) { if (action == nullptr) { return; } QMenu *popup = mImageCanvas->contextMenu(); if (popup == nullptr) { return; } if (sepBefore) { popup->addSeparator(); } popup->addAction(action); } void KookaView::connectGalleryAction(QAction *action, bool sepBefore) { if (action == nullptr) { return; } QMenu *popup = gallery()->contextMenu(); if (popup == nullptr) { return; } if (sepBefore) { popup->addSeparator(); } popup->addAction(action); } void KookaView::connectThumbnailAction(QAction *action) { if (action == nullptr) { return; } QMenu *popup = mThumbView->contextMenu(); if (popup != nullptr) { popup->addAction(action); } } void KookaView::connectPreviewAction(QAction *action) { if (action == nullptr) { return; } QMenu *popup = mPreviewCanvas->getImageCanvas()->contextMenu(); if (popup != nullptr) { popup->addAction(action); } } void KookaView::slotApplySettings() { if (mThumbView != nullptr) { mThumbView->readSettings(); // size and background } if (mGallery != nullptr) { mGallery->readSettings(); // layout and rename } } // Starting a scan or preview, switch tab and tell the scan device void KookaView::slotStartPreview() { if (mScanParams == nullptr) { return; } setCurrentIndex(KookaView::TabScan); qApp->processEvents(); // let the tab appear mScanParams->slotAcquirePreview(); } void KookaView::slotStartFinalScan() { if (mScanParams == nullptr) { return; } setCurrentIndex(KookaView::TabScan); qApp->processEvents(); // let the tab appear mScanParams->slotStartScan(); } void KookaView::slotAutoSelect(bool on) { if (mPreviewCanvas == nullptr) { return; } setCurrentIndex(KookaView::TabScan); qApp->processEvents(); // let the tab appear mPreviewCanvas->slotAutoSelToggled(on); } /* Slot called to start copying to printer */ void KookaView::slotStartPhotoCopy() { //qDebug(); #ifndef KDE4 if (mScanParams == 0) { return; } mIsPhotoCopyMode = true; mPhotoCopyPrinter = new KPrinter(true, QPrinter::HighResolution); // mPhotoCopyPrinter->removeStandardPage( KPrinter::CopiesPage ); mPhotoCopyPrinter->setUsePrinterResolution(true); mPhotoCopyPrinter->setOption(OPT_SCALING, "scan"); mPhotoCopyPrinter->setFullPage(true); slotPhotoCopyScan(KScanDevice::Ok); #endif } void KookaView::slotPhotoCopyPrint(const QImage *img, const ImageMetaInfo *info) { //qDebug(); #ifndef KDE4 if (! mIsPhotoCopyMode) { return; } KookaImage kooka_img = KookaImage(*img); KookaPrint kookaprint(mPhotoCopyPrinter); kookaprint.printImage(&kooka_img , 0); #endif } void KookaView::slotPhotoCopyScan(KScanDevice::Status status) { //qDebug(); #ifndef KDE4 if (! mIsPhotoCopyMode) { return; } // mPhotoCopyPrinter->addDialogPage( new PhotoCopyPrintDialogPage( mScanDevice ) ); KScanOption res(SANE_NAME_SCAN_RESOLUTION); mPhotoCopyPrinter->setOption(OPT_SCAN_RES, res.get()); mPhotoCopyPrinter->setMargins(QSize(0, 0)); //qDebug() << "Resolution" << res.get(); // mPhotoCopyPrinter->addDialogPage( new ImgPrintDialog( 0 ) ); if (mPhotoCopyPrinter->setup(0, "Photocopy")) { Q_CHECK_PTR(mScanDevice); mScanParams->slotStartScan(); } else { mIsPhotoCopyMode = false; delete mPhotoCopyPrinter; } #endif } ScanGallery *KookaView::gallery() const { return (mGallery->galleryTree()); } ImageCanvas *KookaView::imageViewer() const { return (mImageCanvas); } Previewer *KookaView::previewer() const { return (mPreviewCanvas); } void KookaView::imageViewerAction(ImageCanvas::UserAction act) { if (mCurrentTab == KookaView::TabScan) { // Scan mPreviewCanvas->getImageCanvas()->performUserAction(act); } else { // Gallery or OCR mImageCanvas->performUserAction(act); } } void KookaView::showOpenWithMenu(KActionMenu *menu) { FileTreeViewItem *curr = gallery()->highlightedFileTreeViewItem(); QString mimeType = curr->fileItem()->mimetype(); //qDebug() << "Trying to open" << curr->url() << "which is" << mimeType; menu->menu()->clear(); int i = 0; mOpenWithOffers = KMimeTypeTrader::self()->query(mimeType); for (KService::List::ConstIterator it = mOpenWithOffers.constBegin(); it != mOpenWithOffers.constEnd(); ++it, ++i) { KService::Ptr service = (*it); //qDebug() << "> offer:" << (*it)->name(); QString actionName((*it)->name().replace("&", "&&")); QAction *act = new QAction(QIcon::fromTheme((*it)->icon()), actionName, this); connect(act, &QAction::triggered, this, [=]() { slotOpenWith(i); }); menu->addAction(act); } menu->menu()->addSeparator(); QAction *act = new QAction(i18n("Other..."), this); connect(act, &QAction::triggered, this, [=]() { slotOpenWith(-1); }); menu->addAction(act); } void KookaView::slotOpenWith(int idx) { //qDebug() << "idx" << idx; FileTreeViewItem *ftvi = gallery()->highlightedFileTreeViewItem(); if (ftvi == nullptr) { return; } QList urllist; urllist.append(ftvi->url()); if (idx!=-1) // application from the menu { Q_ASSERT(idx 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 KOOKAVIEW_H #define KOOKAVIEW_H #include #include #include "statusbarmanager.h" #include "imagecanvas.h" #include "kscandevice.h" class QSplitter; class QUrl; class KPrinter; class QAction; class KMainWindow; class KActionMenu; class ThumbView; class KookaImage; class KookaGallery; class OcrResEdit; class ScanGallery; class KookaScanParams; class ImageMetaInfo; class Previewer; class KScanDevice; class ImageCanvas; class WidgetSite; /** * This is the main view class for Kooka. Most of the non-menu, * non-toolbar, and non-statusbar (e.g., non frame) GUI code should go * here. * * @short Main view * @author Klaas Freitag * @version 0.1 */ class KookaView : public QTabWidget { Q_OBJECT public: enum MirrorType { MirrorVertical, MirrorHorizontal, MirrorBoth }; enum TabPage { TabScan = 0, TabGallery = 1, TabOcr = 2, TabNone = 0xFF }; /** * To avoid a proliferation of boolean parameters, these flags are * used to indicate the state of the gallery and image viewers. */ enum StateFlag { GalleryShown = 0x001, // in Gallery/OCR mode PreviewValid = 0x002, // scan preview valid ImageValid = 0x004, // viewer image loaded IsDirectory = 0x008, // directory selected FileSelected = 0x010, // 1 file selected ManySelected = 0x020, // multiple selection RootSelected = 0x040, // root is selected IsSubImage = 0x080, // a subimage selected HasSubImages = 0x100 // contains subimages }; Q_DECLARE_FLAGS(StateFlags, StateFlag) /** * Default constructor */ explicit KookaView(KMainWindow *parent, const QByteArray &deviceToUse); /** * Destructor */ virtual ~KookaView(); /** * Print this view to any medium -- paper or not */ void print(); void loadStartupImage(); ScanGallery *gallery() const; ImageCanvas *imageViewer() const; Previewer *previewer() const; bool isScannerConnected() const; QString scannerName() const; void closeScanDevice(); void connectViewerAction(QAction *action, bool sepBefore = false); void connectGalleryAction(QAction *action, bool sepBefore = false); void connectThumbnailAction(QAction *action); void connectPreviewAction(QAction *action); void imageViewerAction(ImageCanvas::UserAction act); void saveWindowSettings(KConfigGroup &grp); void applyWindowSettings(const KConfigGroup &grp); public slots: void slotStartOcr(); void slotStartOcrSelection(); void slotSetOcrSpellConfig(const QString &configFile); void slotOcrSpellCheck(bool interactive = true, bool background = false); void slotSaveOcrResult(); void slotStartPreview(); void slotNewPreview(const QImage *newimg, const ImageMetaInfo *info); void slotStartFinalScan(); void slotAutoSelect(bool on); void slotCreateNewImgFromSelection(); void slotTransformImage(); void slotScanParams(); void slotApplySettings(); /** * slot to select the scanner device. Does all the work with selection * of scanner, disconnection of the old device and connecting the new. */ bool slotSelectDevice(const QByteArray &useDevice = "", bool alwaysAsk = true); void slotAddDevice(); void slotScanStart(const ImageMetaInfo *info); void slotScanFinished(KScanDevice::Status stat); void slotAcquireStart(); void showOpenWithMenu(KActionMenu *menu); protected slots: void slotStartPhotoCopy(); void slotPhotoCopyPrint(const QImage *img, const ImageMetaInfo *info); void slotPhotoCopyScan(KScanDevice::Status); void slotShowAImage(const KookaImage *img, bool isDir); void slotUnloadAImage(const KookaImage *img); /** * called from the scandevice if a new Image was successfully scanned. * Needs to convert the one-page-QImage to a KookaImage */ void slotNewImageScanned(const QImage *img, const ImageMetaInfo *info); void slotSelectionChanged(const QRect &newSelection); void slotGallerySelectionChanged(); void slotOcrResultAvailable(); void slotTabChanged(int index); signals: /** * Change the content of the statusbar */ void changeStatus(const QString &text, StatusBarManager::Item item = StatusBarManager::Message); /** * Clean up the statusbar */ void clearStatus(StatusBarManager::Item item = StatusBarManager::Message); /** * Use this signal to change the content of the caption */ void signalChangeCaption(const QString &text); void signalViewSelectionState(KookaView::StateFlags state); void signalScannerChanged(bool haveConnection); void signalRectangleChanged(bool haveSelection); void signalOcrResultAvailable(bool haveText); void signalOcrPrefs(); private: - void startOCR(const KookaImage img); + void startOCR(const KookaImage &img); QByteArray userDeviceSelection(bool alwaysAsk); void saveGalleryState(int index = -1) const; void restoreGalleryState(int index = -1); void updateSelectionState(); private slots: void slotStartLoading(const QUrl &url); void slotOpenWith(int idx); void slotPreviewDimsChanged(const QString &dims); private: KMainWindow *mMainWindow; ImageCanvas *mImageCanvas; ThumbView *mThumbView; Previewer *mPreviewCanvas; KookaGallery *mGallery; KookaScanParams *mScanParams; KScanDevice *mScanDevice; QImage *mOcrResultImg; OcrResEdit *mOcrResEdit; bool mIsPhotoCopyMode; KPrinter *mPhotoCopyPrinter; KookaView::TabPage mCurrentTab; QSplitter *mScanPage; QSplitter *mScanSubSplitter; QSplitter *mGalleryPage; QSplitter *mGallerySubSplitter; QSplitter *mOcrPage; QSplitter *mOcrSubSplitter; WidgetSite *mParamsSite; WidgetSite *mScanGallerySite; WidgetSite *mGalleryGallerySite; WidgetSite *mOcrGallerySite; WidgetSite *mGalleryImgviewSite; WidgetSite *mOcrImgviewSite; KService::List mOpenWithOffers; }; Q_DECLARE_OPERATORS_FOR_FLAGS(KookaView::StateFlags) #endif // KOOKAVIEW_H diff --git a/plugins/ocr/abstractocrengine.cpp b/plugins/ocr/abstractocrengine.cpp index f685e11..85c0f7b 100644 --- a/plugins/ocr/abstractocrengine.cpp +++ b/plugins/ocr/abstractocrengine.cpp @@ -1,596 +1,596 @@ /************************************************************************ * * * 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) +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; 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 01f4c33..cd2a8de 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 }; 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 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