File indexing completed on 2025-01-12 12:39:36
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 }