File indexing completed on 2024-04-21 15:12:12

0001 /************************************************************************
0002  *                                  *
0003  *  This file is part of Kooka, a scanning/OCR application using    *
0004  *  Qt <http://www.qt.io> and KDE Frameworks <http://www.kde.org>.  *
0005  *                                  *
0006  *  Copyright (C) 2000-2016 Klaas Freitag <freitag@suse.de>     *
0007  *                          Jonathan Marten <jjm@keelhaul.me.uk>    *
0008  *                                  *
0009  *  Kooka is free software; you can redistribute it and/or modify it    *
0010  *  under the terms of the GNU Library General Public License as    *
0011  *  published by the Free Software Foundation and appearing in the  *
0012  *  file COPYING included in the packaging of this file;  either    *
0013  *  version 2 of the License, or (at your option) any later version.    *
0014  *                                  *
0015  *  As a special exception, permission is given to link this program    *
0016  *  with any version of the KADMOS OCR/ICR engine (a product of     *
0017  *  reRecognition GmbH, Kreuzlingen), and distribute the resulting  *
0018  *  executable without including the source code for KADMOS in the  *
0019  *  source distribution.                        *
0020  *                                  *
0021  *  This program is distributed in the hope that it will be useful, *
0022  *  but WITHOUT ANY WARRANTY; without even the implied warranty of  *
0023  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the   *
0024  *  GNU General Public License for more details.            *
0025  *                                  *
0026  *  You should have received a copy of the GNU General Public       *
0027  *  License along with this program;  see the file COPYING.  If     *
0028  *  not, see <http://www.gnu.org/licenses/>.                *
0029  *                                  *
0030  ************************************************************************/
0031 
0032 #include "previewer.h"
0033 
0034 #include <qvector.h>
0035 #include <qtimer.h>
0036 #include <qlayout.h>
0037 
0038 #include <kwidgetsaddons_version.h>
0039 #include <klocalizedstring.h>
0040 #include <kmessagebox.h>
0041 
0042 #ifdef DEBUG_AUTOSEL
0043 #include <qfile.h>
0044 #include <qtextstream.h>
0045 #endif
0046 
0047 #include "imagecanvas.h"
0048 #include "kscandevice.h"
0049 #include "autoselectbar.h"
0050 #include "scansettings.h"
0051 #include "libkookascan_logging.h"
0052 
0053 
0054 Previewer::Previewer(QWidget *parent)
0055     : QFrame(parent)
0056 {
0057     setObjectName("Previewer");
0058 
0059     QVBoxLayout *vbl = new QVBoxLayout(this);
0060     vbl->setMargin(0);
0061 
0062     /* Units etc. TODO: get from Config */
0063     mDisplayUnit = KRuler::Millimetres;
0064     mAutoSelThresh = 25;                // not used until scanner connected
0065 
0066     mScanDevice = nullptr;                 // no scanner connected yet
0067     mBedHeight = 297;                   // for most A4/Letter scanners
0068     mBedWidth = 215;
0069 
0070     // Image viewer
0071     mCanvas  = new ImageCanvas(this);
0072     mCanvas->setDefaultScaleType(ImageCanvas::ScaleDynamic);
0073     vbl->addWidget(mCanvas);
0074 
0075     /*Signals: Control the custom-field and show size of selection */
0076     connect(mCanvas, QOverload<const QRectF &>::of(&ImageCanvas::newRect), this, &Previewer::slotNewAreaSelected);
0077 
0078     mAutoSelectBar = new AutoSelectBar(mAutoSelThresh, this);
0079     connect(mAutoSelectBar, &AutoSelectBar::thresholdChanged, this, &Previewer::slotSetAutoSelThresh);
0080     connect(mAutoSelectBar, &AutoSelectBar::advancedSettingsChanged, this, &Previewer::slotAutoSelectSettingsChanged);
0081     connect(mAutoSelectBar, &AutoSelectBar::performSelection, this, &Previewer::slotFindAutoSelection);
0082     vbl->addWidget(mAutoSelectBar);
0083 
0084     mScanResX = -1;
0085     mScanResY = -1;
0086     mBytesPerPix = -1;
0087 
0088     mSelectionWidthMm = mBedWidth;
0089     mSelectionHeightMm = mBedHeight;
0090     updateSelectionDims();
0091     setAutoSelection(false);                // initially disable, no scanner
0092 }
0093 
0094 
0095 bool Previewer::setPreviewImage(ScanImage::Ptr image)
0096 {
0097     if (image.isNull()) return (false);
0098 
0099     qCDebug(LIBKOOKASCAN_LOG) << "setting new image, size" << image->size();
0100     mCanvas->newImage(image);
0101     return (true);
0102 }
0103 
0104 
0105 void Previewer::newImage(ScanImage::Ptr image)
0106 {
0107     if (image.isNull()) return;
0108 
0109     resetAutoSelection();               // reset for new image
0110     mCanvas->newImage(image);               // set image on canvas
0111     slotFindAutoSelection();                // auto-select if required
0112     slotNotifyAutoSelectChanged();          // tell the GUI
0113 }
0114 
0115 
0116 void Previewer::setScannerBedSize(int w, int h)
0117 {
0118     //qCDebug(LIBKOOKASCAN_LOG) << "to [" << w << "," << h << "]";
0119 
0120     mBedWidth = w;
0121     mBedHeight = h;
0122 
0123     slotNewCustomScanSize(QRect());         // reset selection and display
0124 }
0125 
0126 void Previewer::setDisplayUnit(KRuler::MetricStyle unit)
0127 {
0128     // TODO: this is not used
0129     mDisplayUnit = unit;
0130 }
0131 
0132 // Signal sent from ScanParams, selection chosen by user
0133 // from scan size combo (specified in integer millimetres)
0134 void Previewer::slotNewCustomScanSize(const QRect &rect)
0135 {
0136     //qCDebug(LIBKOOKASCAN_LOG) << "rect" << rect;
0137 
0138     QRect r = rect;
0139     QRectF newRect;
0140     if (r.isValid()) {
0141         mSelectionWidthMm = r.width();
0142         mSelectionHeightMm = r.height();
0143 
0144         newRect.setLeft(double(r.left()) / mBedWidth);  // convert mm -> bedsize factor
0145         newRect.setWidth(double(r.width()) / mBedWidth);
0146         newRect.setTop(double(r.top()) / mBedHeight);
0147         newRect.setHeight(double(r.height()) / mBedHeight);
0148     } else {
0149         mSelectionWidthMm = mBedWidth;
0150         mSelectionHeightMm = mBedHeight;
0151     }
0152 
0153     mCanvas->setSelectionRect(newRect);
0154     updateSelectionDims();
0155 }
0156 
0157 void Previewer::slotNewScanResolutions(int xres, int yres)
0158 {
0159     qCDebug(LIBKOOKASCAN_LOG) << "resolution" << xres << "x" << yres;
0160 
0161     mScanResX = xres;
0162     mScanResY = yres;
0163     updateSelectionDims();
0164 }
0165 
0166 void Previewer::slotNewScanMode(int bytes_per_pix)
0167 {
0168     qCDebug(LIBKOOKASCAN_LOG) << "bytes per pix" << bytes_per_pix;
0169 
0170     mBytesPerPix = bytes_per_pix;
0171     updateSelectionDims();
0172 }
0173 
0174 //  Signal sent from ImageCanvas, the user has drawn a new selection box
0175 //  (specified relative to the preview image size).  This is converted
0176 //  to millimetres and re-emitted as signal newPreviewRect().
0177 void Previewer::slotNewAreaSelected(const QRectF &rect)
0178 {
0179     qCDebug(LIBKOOKASCAN_LOG) << "rect" << rect << "width" << mBedWidth << "height" << mBedHeight;
0180 
0181     if (rect.isValid()) {
0182         // convert bedsize -> mm
0183         QRect r;
0184         r.setLeft(qRound(rect.left()*mBedWidth));
0185         r.setWidth(qRound(rect.width()*mBedWidth));
0186         r.setTop(qRound(rect.top()*mBedHeight));
0187         r.setHeight(qRound(rect.height()*mBedHeight));
0188         //qCDebug(LIBKOOKASCAN_LOG) << "new rect" << r;
0189         emit newPreviewRect(r);
0190 
0191         mSelectionWidthMm = r.width();
0192         mSelectionHeightMm = r.height();
0193     } else {
0194         emit newPreviewRect(QRect());           // full scan area
0195 
0196         mSelectionWidthMm = mBedWidth;          // for size display
0197         mSelectionHeightMm = mBedHeight;
0198     }
0199 
0200     updateSelectionDims();
0201 }
0202 
0203 static inline int mmToPixels(double mm, int res)
0204 {
0205     return (qRound(mm / 25.4 * res));
0206 }
0207 
0208 void Previewer::updateSelectionDims()
0209 {
0210     if (mScanDevice == nullptr) {
0211         return;    // no scanner connected
0212     }
0213 
0214     /* Calculate file size */
0215     if (mScanResX > 1 && mScanResY > 1) {   // if resolution available
0216         if (mBytesPerPix != -1) {           // depth of scan available?
0217             int wPix = mmToPixels(mSelectionWidthMm, mScanResX);
0218             int hPix = mmToPixels(mSelectionHeightMm, mScanResY);
0219 
0220             long size_in_byte;
0221             if (mBytesPerPix == 0) {        // bitmap scan
0222                 size_in_byte = wPix * hPix / 8;
0223             } else {                // grey or colour scan
0224                 size_in_byte = wPix * hPix * mBytesPerPix;
0225             }
0226 
0227             emit previewFileSizeChanged(size_in_byte);
0228         } else {
0229             emit previewFileSizeChanged(-1);    // depth not available
0230         }
0231     }
0232 
0233     QString result = previewInfoString(mSelectionWidthMm, mSelectionHeightMm, mScanResX, mScanResY);
0234     emit previewDimsChanged(result);
0235 }
0236 
0237 QString Previewer::previewInfoString(double widthMm, double heightMm, int resX, int resY)
0238 {
0239     if (resX > 1 && resY > 1) {         // resolution available
0240         int wPix = mmToPixels(widthMm, resX);
0241         int hPix = mmToPixels(heightMm, resY);
0242         // xgettext:no-c-format
0243         return (i18nc("@info:status", "%1x%2mm, %3x%4pix", widthMm, heightMm, wPix, hPix));
0244     } else {                    // resolution not available
0245         // xgettext:no-c-format
0246         return (i18nc("@info:status", "%1x%2mm", widthMm, heightMm));
0247     }
0248 }
0249 
0250 void Previewer::connectScanner(KScanDevice *scan)
0251 {
0252     qCDebug(LIBKOOKASCAN_LOG) << scan->scannerBackendName();
0253     mScanDevice = scan;
0254     mCanvas->newImage(nullptr);             // remove previous preview
0255 
0256     if (scan != nullptr) {
0257         setAutoSelection(false);            // initially off, disregard config
0258                             // but get other saved values
0259         const KConfigSkeletonItem *item = ScanSettings::self()->previewAutoselBackgroundItem();
0260         mBgIsWhite = (scan->getConfig<int>(item)!=ScanSettings::BackgroundBlack);
0261 
0262         item = ScanSettings::self()->previewAutoselThresholdItem();
0263         int val = scan->getConfig<int>(item);
0264         mAutoSelThresh = val;
0265         mAutoSelectBar->setThreshold(val);
0266 
0267         item = ScanSettings::self()->previewAutoselDustsizeItem();
0268         val = scan->getConfig<int>(item);
0269         mAutoSelDustsize = val;
0270 
0271         item = ScanSettings::self()->previewAutoselMarginItem();
0272         val = scan->getConfig<int>(item);
0273         mAutoSelMargin = val;
0274 
0275         //qCDebug(LIBKOOKASCAN_LOG) << "margin" << mAutoSelMargin << "white?" << mBgIsWhite << "dust" << mAutoSelDustsize;
0276         mAutoSelectBar->setAdvancedSettings(mAutoSelMargin, mBgIsWhite, mAutoSelDustsize);
0277 
0278         updateSelectionDims();
0279     }
0280 }
0281 
0282 void Previewer::slotNotifyAutoSelectChanged()
0283 {
0284     const bool isAvailable = mCanvas->hasImage();
0285     const bool isOn = mDoAutoSelection;
0286     emit autoSelectStateChanged(isAvailable, isOn);
0287 }
0288 
0289 void Previewer::resetAutoSelection()
0290 {
0291     mHeightSum.clear();
0292     mWidthSum.clear();
0293 }
0294 
0295 void Previewer::setAutoSelection(bool isOn)
0296 {
0297     qCDebug(LIBKOOKASCAN_LOG) << "to" << isOn;
0298 
0299     if (isOn && mScanDevice == nullptr) {           // no scanner connected yet
0300         qCWarning(LIBKOOKASCAN_LOG) << "no scanner!";
0301         isOn = false;
0302     }
0303 
0304     mDoAutoSelection = isOn;
0305     if (mAutoSelectBar != nullptr) {
0306         mAutoSelectBar->setVisible(isOn);
0307     }
0308     if (mScanDevice != nullptr) {
0309         const KConfigSkeletonItem *item = ScanSettings::self()->previewAutoselOnItem();
0310         mScanDevice->storeConfig<bool>(item, isOn);
0311     }
0312                             // tell the GUI
0313     QTimer::singleShot(0, this, &Previewer::slotNotifyAutoSelectChanged);
0314 }
0315 
0316 /**
0317  * reads the scanner dependent config file through the mScanDevice pointer.
0318  * If a value for the scanner is not yet known, the function starts up a
0319  * popup and asks the user.  The result is stored.
0320  */
0321 
0322 bool Previewer::checkForScannerBg()
0323 {
0324     if (mScanDevice == nullptr) {
0325         return (true);    // no scan device
0326     }
0327 
0328     KConfigSkeletonItem *item = ScanSettings::self()->previewAutoselBackgroundItem();
0329     int curWhite = mScanDevice->getConfig<int>(item);
0330     bool goWhite = false;
0331 
0332     if (curWhite==ScanSettings::BackgroundUnknown)  // not yet known, ask the user
0333     {
0334         qCDebug(LIBKOOKASCAN_LOG) << "Don't know the scanner background yet";
0335 #if KWIDGETSADDONS_VERSION >= QT_VERSION_CHECK(5, 100, 0)
0336         int res = KMessageBox::questionTwoActionsCancel(this,
0337 #else
0338         int res = KMessageBox::questionYesNoCancel(this,
0339 #endif
0340                                                    i18n("The autodetection of images on the preview depends on the background color of the preview image (the result of scanning with no document loaded).\n\nPlease select whether the background of the preview image is black or white."),
0341                                                    i18nc("@title:window", "Autodetection Background"),
0342                                                    KGuiItem(i18nc("@action:button Name of colour", "White")),
0343                                                    KGuiItem(i18nc("@action:button Name of colour", "Black")));
0344         if (res==KMessageBox::Cancel) return (false);
0345 #if KWIDGETSADDONS_VERSION >= QT_VERSION_CHECK(5, 100, 0)
0346         goWhite = (res==KMessageBox::PrimaryAction);
0347 #else
0348         goWhite = (res==KMessageBox::Yes);
0349 #endif
0350     }
0351     else if (curWhite==ScanSettings::BackgroundWhite) goWhite = true;
0352 
0353     mBgIsWhite = goWhite;               // set and save that value
0354     mScanDevice->storeConfig<int>(item, (goWhite ? ScanSettings::BackgroundWhite : ScanSettings::BackgroundBlack));
0355 
0356     resetAutoSelection();               // invalidate image data
0357     return (true);
0358 }
0359 
0360 void Previewer::slotAutoSelToggled(bool isOn)
0361 {
0362     if (isOn) {                     // turning the option on?
0363         if (!checkForScannerBg()) {         // check or ask for background
0364             // if couldn't resolve it
0365             setAutoSelection(false);            // then force option off
0366             return;                 // and give up here
0367         }
0368     }
0369 
0370     setAutoSelection(isOn);             // set and store setting
0371     if (isOn && !mCanvas->hasSelectedRect()) {      // no selection yet?
0372         if (mCanvas->hasImage()) {          // is there a preview?
0373             qCDebug(LIBKOOKASCAN_LOG) << "No selection, try to find one";
0374             slotFindAutoSelection();
0375         }
0376     }
0377 }
0378 
0379 void Previewer::slotSetAutoSelThresh(int t)
0380 {
0381     mAutoSelThresh = t;
0382     qCDebug(LIBKOOKASCAN_LOG) << "Setting threshold to" << t;
0383 
0384     if (mScanDevice!=nullptr)
0385     {
0386         const KConfigSkeletonItem *item = ScanSettings::self()->previewAutoselThresholdItem();
0387         mScanDevice->storeConfig<int>(item, t);
0388     }
0389 
0390     slotFindAutoSelection();                // with new setting
0391 }
0392 
0393 void Previewer::slotAutoSelectSettingsChanged(int margin, bool bgIsWhite, int dustsize)
0394 {
0395     qCDebug(LIBKOOKASCAN_LOG) << "margin" << margin << "white?" << bgIsWhite << "dust" << dustsize;
0396 
0397     if (mScanDevice!=nullptr)               // save settings for scanner
0398     {
0399         const KConfigSkeletonItem *item = ScanSettings::self()->previewAutoselMarginItem();
0400         mScanDevice->storeConfig<int>(item, margin);
0401 
0402         item = ScanSettings::self()->previewAutoselBackgroundItem();
0403         mScanDevice->storeConfig<int>(item, (bgIsWhite ? ScanSettings::BackgroundWhite : ScanSettings::BackgroundBlack));
0404 
0405         item = ScanSettings::self()->previewAutoselDustsizeItem();
0406         mScanDevice->storeConfig<int>(item, dustsize);
0407     }
0408 
0409     mAutoSelMargin = margin;                // set area margin
0410     mAutoSelDustsize = dustsize;            // set dust size
0411 
0412     if (bgIsWhite != mBgIsWhite) {          // changing this setting?
0413         mBgIsWhite = bgIsWhite;             // set background colour
0414         resetAutoSelection();               // invalidate image data
0415     }
0416 
0417     slotFindAutoSelection();                // find with new settings
0418 }
0419 
0420 //  Try to automatically find a selection on the preview image.
0421 //  It uses the image of the preview image canvas, the threshold
0422 //  setting and a dust size.
0423 //
0424 //  Each individual scan line and column of the image is separately
0425 //  averaged into a greyscale pixel value.  Each of these sets is
0426 //  then scanned by imagePiece() to identify runs of above-threshold
0427 //  (for a black scanner background) or below-threshold (for white
0428 //  background) areas which are longer than the dust size setting.
0429 //  The longest of those found becomes the auto-selected area.
0430 
0431 void Previewer::slotFindAutoSelection()
0432 {
0433     if (!mDoAutoSelection) return;          // not doing auto selection
0434 
0435     const QImage *img = mCanvas->rootImage().data();
0436     if (img==nullptr || img->isNull()) return;      // must have an image
0437 
0438     qCDebug(LIBKOOKASCAN_LOG) << "image size" << img->size()
0439                               << "threshold" << mAutoSelThresh
0440                               << "dustsize" << mAutoSelDustsize
0441                               << "isWhite" << mBgIsWhite;
0442 
0443     const long iWidth  = img->width();          // cheap copies
0444     const long iHeight = img->height();
0445 
0446     if (mHeightSum.isEmpty()) {             // need to initialise arrays
0447         //qCDebug(LIBKOOKASCAN_LOG) << "Summing image data";
0448         QVector<long> heightSum(iHeight);
0449         QVector<long> widthSum(iWidth);
0450         heightSum.fill(0);
0451         widthSum.fill(0);
0452 
0453         // Sum scan lines in both directions
0454         for (int y = 0; y < iHeight; ++y) {
0455             for (int x = 0; x < iWidth; ++x) {
0456                 int pixgray = qGray(img->pixel(x, y));
0457                 widthSum[x] += pixgray;
0458                 heightSum[y] += pixgray;
0459             }
0460         }
0461 
0462         // Normalize sums (divide summed values by the total pixels)
0463         // to get an average for each scan line.
0464         // If the background is white, then also invert the values here.
0465         for (int x = 0; x < iWidth; ++x) {
0466             long sumval = widthSum[x];
0467             sumval /= iHeight;
0468             if (mBgIsWhite) {
0469                 sumval = 255 - sumval;
0470             }
0471             widthSum[x] = sumval;
0472         }
0473         for (int y = 0; y < iHeight; ++y) {
0474             long sumval = heightSum[y];
0475             sumval /= iWidth;
0476             if (mBgIsWhite) {
0477                 sumval = 255 - sumval;
0478             }
0479             heightSum[y] = sumval;
0480         }
0481 
0482         mWidthSum  = widthSum;              // also resizes them
0483         mHeightSum = heightSum;
0484     }
0485 
0486     /* Now try to find values in arrays that have grayAdds higher or lower
0487      * than threshold */
0488 #ifdef DEBUG_AUTOSEL
0489     /* debug output */
0490     {
0491         QFile fi("/tmp/thheight.dat");
0492         if (fi.open(IO_ReadWrite)) {
0493             QTextStream str(&fi);
0494 
0495             str << "# height ##################" << endl;
0496             for (x = 0; x < iHeight; x++) {
0497                 str << x << '\t' << mHeightSum[x] << endl;
0498             }
0499             fi.close();
0500         }
0501     }
0502     QFile fi1("/tmp/thwidth.dat");
0503     if (fi1.open(IO_ReadWrite)) {
0504         QTextStream str(&fi1);
0505         str << "# width ##################" << endl;
0506         str << "# " << iWidth << " points" << endl;
0507         for (x = 0; x < iWidth; x++) {
0508             str << x << '\t' << mWidthSum[x] << endl;
0509         }
0510 
0511         fi1.close();
0512     }
0513 #endif
0514     int start;
0515     int end;
0516     QRectF r;
0517 
0518     if (imagePiece(mHeightSum, &start, &end)) {
0519         double margin = double(mAutoSelMargin) / mBedHeight;
0520         r.setTop(qMax(((double(start) / iHeight) - margin), 0.0));
0521         r.setBottom(qMin(((double(end) / iHeight) + margin), 0.999999));
0522     }
0523 
0524     if (imagePiece(mWidthSum, &start, &end)) {
0525         double margin = double(mAutoSelMargin) / mBedWidth;
0526         r.setLeft(qMax(((double(start) / iWidth) - margin), 0.0));
0527         r.setRight(qMin(((double(end) / iWidth) + margin), 0.999999));;
0528     }
0529 
0530     qCDebug(LIBKOOKASCAN_LOG) << "Autodetection result" << r;
0531     mCanvas->setSelectionRect(r);
0532     slotNewAreaSelected(r);
0533 }
0534 
0535 //  Analyse the specified array of scanline averages and try to identify
0536 //  a run of more than <dustsize> pixels above the <threshold>.  If one
0537 //  can be found, record its start and end as the selection boundaries.
0538 //  The longest such run is returned as the result.
0539 //
0540 //  If the scanner background is white, then the pixel value will already
0541 //  have been inverted by the caller.  The logic here is therefore the
0542 //  same for a black or white background.
0543 
0544 bool Previewer::imagePiece(const QVector<long> &src, int *startp, int *endp)
0545 {
0546     int foundStart = 0;
0547     int foundEnd = 0;
0548 
0549     for (int x = 0; x < src.size(); ++x) {
0550         if (src[x] > mAutoSelThresh) {
0551             int thisStart = x;              // record as possible start
0552             ++x;                    // step on to next
0553             while (x < src.size() && src[x] > mAutoSelThresh) {
0554                 ++x;
0555             }
0556             int thisEnd = x;                // find end and record that
0557 
0558             int delta = thisEnd - thisStart;    // length of this run
0559 
0560             if (delta > mAutoSelDustsize) {     // bigger than dust size?
0561                 if (delta > (foundEnd - foundStart)) { // bigger than previously found?
0562                     foundStart = thisStart;     // record as result so far
0563                     foundEnd = thisEnd;
0564                 }
0565             }
0566         }
0567     }
0568 
0569     *startp = foundStart;
0570     *endp = foundEnd;
0571     return ((foundEnd - foundStart) > 0);
0572 }