KSane

ksaneviewer.cpp
1/*
2 * SPDX-FileCopyrightText: 2008 Kare Sars <kare dot sars at iki dot fi>
3 * SPDX-FileCopyrightText: 2014 Gregor Mitsch : port to KDE5 frameworks
4 *
5 * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
6 */
7
8#include "ksaneviewer.h"
9
10#include "selectionitem.h"
11#include "hiderectitem.h"
12
13#include <QGraphicsPixmapItem>
14#include <QGraphicsScene>
15#include <QGraphicsRectItem>
16#include <QScrollBar>
17#include <QAction>
18#include <QList>
19#include <QVector>
20#include <QIcon>
21
22#include <KLocalizedString>
23
24#include <math.h>
25
26namespace KSaneIface
27{
28
29struct KSaneViewer::Private {
30 QGraphicsScene *scene;
31 SelectionItem *selection;
32 QImage *img;
33
34 QList<SelectionItem *> selectionList;
35 SelectionItem::Intersects change;
36
37 QPointF lastSPoint;
38 int m_left_last_x;
39 int m_left_last_y;
40
41 QAction *zoomInAction;
42 QAction *zoomOutAction;
43 QAction *zoomSelAction;
44 QAction *zoom2FitAction;
45 QAction *clrSelAction;
46
47 HideRectItem *hideLeft;
48 HideRectItem *hideRight;
49 HideRectItem *hideTop;
50 HideRectItem *hideBottom;
51 HideRectItem *hideArea;
52
53 bool multiSelectionEnabled = true;
54
55 int wheelDelta = 0;
56
57 int currentImageWidth;
58 int currentImageHeight;
59
60 QGraphicsPolygonItem * border;
61};
62
63KSaneViewer::KSaneViewer(QImage *img, QWidget *parent) : QGraphicsView(parent), d(new Private)
64{
65 d->img = img;
66
67 setMouseTracking(true);
68
69 // Init the scene
70 d->scene = new QGraphicsScene(this);
71 const auto dpr = img->devicePixelRatio();
72
73 d->currentImageWidth = img->width();
74 d->currentImageHeight = img->height();
75
76 d->scene->setSceneRect(0, 0, d->currentImageWidth / dpr, d->currentImageHeight / dpr);
77 setScene(d->scene);
78
79 d->selection = new SelectionItem(QRectF());
80 d->selection->setZValue(10);
81 d->selection->setSaved(false);
82 d->selection->setMaxRight(d->currentImageWidth);
83 d->selection->setMaxBottom(d->currentImageHeight);
84 d->selection->setRect(d->scene->sceneRect());
85 d->selection->setVisible(false);
86
87 d->hideTop = new HideRectItem;
88 d->hideBottom = new HideRectItem;
89 d->hideRight = new HideRectItem;
90 d->hideLeft = new HideRectItem;
91 d->hideArea = new HideRectItem;
92 d->hideArea->setOpacity(0.6);
93
94 d->scene->addItem(d->selection);
95 d->scene->addItem(d->hideLeft);
96 d->scene->addItem(d->hideRight);
97 d->scene->addItem(d->hideTop);
98 d->scene->addItem(d->hideBottom);
99 d->scene->addItem(d->hideArea);
100
101 QPolygonF polygon(QRectF(QPointF(0,0),QSizeF(d->currentImageWidth, d->currentImageHeight)));
103 d->border = d->scene->addPolygon(polygon, pen);
104
105 d->change = SelectionItem::None;
106 d->selectionList.clear();
107
108 // create context menu
109 d->zoomInAction = new QAction(QIcon::fromTheme(QLatin1String("zoom-in")), i18n("Zoom In"), this);
110 connect(d->zoomInAction, &QAction::triggered, this, &KSaneViewer::zoomIn);
111
112 d->zoomOutAction = new QAction(QIcon::fromTheme(QLatin1String("zoom-out")), i18n("Zoom Out"), this);
113 connect(d->zoomOutAction, &QAction::triggered, this, &KSaneViewer::zoomOut);
114
115 d->zoomSelAction = new QAction(QIcon::fromTheme(QLatin1String("zoom-fit-best")), i18n("Zoom to Selection"), this);
116 connect(d->zoomSelAction, &QAction::triggered, this, &KSaneViewer::zoomSel);
117
118 d->zoom2FitAction = new QAction(QIcon::fromTheme(QLatin1String("document-preview")), i18n("Zoom to Fit"), this);
119 connect(d->zoom2FitAction, &QAction::triggered, this, &KSaneViewer::zoom2Fit);
120
121 d->clrSelAction = new QAction(QIcon::fromTheme(QLatin1String("edit-clear")), i18n("Clear Selections"), this);
122 connect(d->clrSelAction, &QAction::triggered, this, &KSaneViewer::clearSelections);
123
124 addAction(d->zoomInAction);
125 addAction(d->zoomOutAction);
126 addAction(d->zoomSelAction);
127 addAction(d->zoom2FitAction);
128 addAction(d->clrSelAction);
129 setContextMenuPolicy(Qt::ActionsContextMenu);
130
131 setFrameShape(QFrame::NoFrame);
132}
133
134// ------------------------------------------------------------------------
135void KSaneViewer::drawBackground(QPainter *painter, const QRectF &rect)
136{
137 painter->fillRect(rect, QWidget::palette().color(QPalette::Window));
138 QRectF r = rect & sceneRect();
139 const qreal dpr = d->img->devicePixelRatio();
140 QRectF srcRect = QRectF(r.topLeft() * dpr, r.size() * dpr);
141 painter->drawImage(r, *d->img, srcRect);
142 d->border->setPolygon(QPolygonF(QRectF(QPointF(0,0),QSizeF(d->currentImageWidth, d->currentImageHeight))));
143}
144
145// ------------------------------------------------------------------------
146KSaneViewer::~KSaneViewer()
147{
148 // first remove any old saved selections
149 clearSavedSelections();
150
151 delete d;
152}
153
154// ------------------------------------------------------------------------
155int KSaneViewer::currentImageHeight() const
156{
157 return d->currentImageHeight;
158}
159
160// ------------------------------------------------------------------------
161int KSaneViewer::currentImageWidth() const
162{
163 return d->currentImageWidth;
164}
165
166// ------------------------------------------------------------------------
167void KSaneViewer::setQImage(QImage *img)
168{
169 if (img == nullptr) {
170 return;
171 }
172
173 // remove selections
174 clearSelections();
175
176 // clear zoom
177 resetTransform();
178
179 const auto dpr = img->devicePixelRatio();
180
181 d->currentImageWidth = img->width();
182 d->currentImageHeight = img->height();
183
184 d->scene->setSceneRect(0, 0, d->currentImageWidth / dpr, d->currentImageHeight / dpr);
185 d->selection->setMaxRight(d->currentImageWidth);
186 d->selection->setMaxBottom(d->currentImageHeight);
187
188 d->selection->setDevicePixelRatio(dpr);
189 d->hideTop->setDevicePixelRatio(dpr);
190 d->hideBottom->setDevicePixelRatio(dpr);
191 d->hideRight->setDevicePixelRatio(dpr);
192 d->hideLeft->setDevicePixelRatio(dpr);
193 d->hideArea->setDevicePixelRatio(dpr);
194
195 d->img = img;
196}
197
198// ------------------------------------------------------------------------
199void KSaneViewer::updateImage()
200{
201 setCacheMode(QGraphicsView::CacheNone);
202 repaint();
203 setCacheMode(QGraphicsView::CacheBackground);
204}
205
206// ------------------------------------------------------------------------
207void KSaneViewer::zoomIn()
208{
209 scale(1.3, 1.3);
210 d->selection->saveZoom(transform().m11());
211 for (int i = 0; i < d->selectionList.size(); ++i) {
212 d->selectionList[i]->saveZoom(transform().m11());
213 }
214}
215
216// ------------------------------------------------------------------------
217void KSaneViewer::zoomOut()
218{
219 scale(1.0 / 1.3, 1.0 / 1.3);
220 d->selection->saveZoom(transform().m11());
221 for (int i = 0; i < d->selectionList.size(); ++i) {
222 d->selectionList[i]->saveZoom(transform().m11());
223 }
224}
225
226// ------------------------------------------------------------------------
227void KSaneViewer::zoomSel()
228{
229 if (d->selection->isVisible()) {
230 fitInView(d->selection->boundingRect() , Qt::KeepAspectRatio);
231 d->selection->saveZoom(transform().m11());
232 for (int i = 0; i < d->selectionList.size(); ++i) {
233 d->selectionList[i]->saveZoom(transform().m11());
234 }
235 } else {
236 zoom2Fit();
237 }
238}
239
240// ------------------------------------------------------------------------
241void KSaneViewer::zoom2Fit()
242{
243 fitInView(d->img->rect(), Qt::KeepAspectRatio);
244 d->selection->saveZoom(transform().m11());
245 for (int i = 0; i < d->selectionList.size(); ++i) {
246 d->selectionList[i]->saveZoom(transform().m11());
247 }
248}
249
250// ------------------------------------------------------------------------
251void KSaneViewer::setTLX(float ratio)
252{
253 if (!d->selection->isVisible()) {
254 return; // only correct the selection if it is visible
255 }
256 QRectF rect = d->selection->rect();
257 rect.setLeft(ratio * d->img->width());
258 d->selection->setRect(rect);
259 updateSelVisibility();
260}
261
262// ------------------------------------------------------------------------
263void KSaneViewer::setTLY(float ratio)
264{
265 if (!d->selection->isVisible()) {
266 return; // only correct the selection if it is visible
267 }
268 QRectF rect = d->selection->rect();
269 rect.setTop(ratio * d->img->height());
270 d->selection->setRect(rect);
271 updateSelVisibility();
272}
273
274// ------------------------------------------------------------------------
275void KSaneViewer::setBRX(float ratio)
276{
277 if (!d->selection->isVisible()) {
278 return; // only correct the selection if it is visible
279 }
280 QRectF rect = d->selection->rect();
281 rect.setRight(ratio * d->img->width());
282 d->selection->setRect(rect);
283 updateSelVisibility();
284}
285
286// ------------------------------------------------------------------------
287void KSaneViewer::setBRY(float ratio)
288{
289 if (!d->selection->isVisible()) {
290 return; // only correct the selection if it is visible
291 }
292 QRectF rect = d->selection->rect();
293 rect.setBottom(ratio * d->img->height());
294 d->selection->setRect(rect);
295 updateSelVisibility();
296}
297
298// ------------------------------------------------------------------------
299void KSaneViewer::setSelection(float tl_x, float tl_y, float br_x, float br_y)
300{
301 QRectF rect;
302 rect.setCoords(tl_x * d->img->width(),
303 tl_y * d->img->height(),
304 br_x * d->img->width(),
305 br_y * d->img->height());
306
307 d->selection->setRect(rect);
308 updateSelVisibility();
309}
310
311// ------------------------------------------------------------------------
312void KSaneViewer::setHighlightArea(float tl_x, float tl_y, float br_x, float br_y)
313{
314 QRectF rect;
315
316 // Left reason for rect: setCoords(x1,y1,x2,y2) != setRect(x1,x2, width, height)
317 rect.setCoords(0, 0, tl_x * d->img->width(), d->img->height());
318 d->hideLeft->setRect(rect);
319
320 // Right
321 rect.setCoords(br_x * d->img->width(),
322 0,
323 d->img->width(),
324 d->img->height());
325 d->hideRight->setRect(rect);
326
327 // Top
328 rect.setCoords(tl_x * d->img->width(),
329 0,
330 br_x * d->img->width(),
331 tl_y * d->img->height());
332 d->hideTop->setRect(rect);
333
334 // Bottom
335 rect.setCoords(tl_x * d->img->width(),
336 br_y * d->img->height(),
337 br_x * d->img->width(),
338 d->img->height());
339 d->hideBottom->setRect(rect);
340
341 // hide area
342 rect.setCoords(tl_x * d->img->width(), tl_y * d->img->height(),
343 br_x * d->img->width(), br_y * d->img->height());
344
345 d->hideArea->setRect(rect);
346
347 d->hideLeft->show();
348 d->hideRight->show();
349 d->hideTop->show();
350 d->hideBottom->show();
351 // the hide area is hidden until setHighlightShown is called.
352 d->hideArea->hide();
353}
354
355// ------------------------------------------------------------------------
356void KSaneViewer::setHighlightShown(int percentage, QColor hideColor)
357{
358 if (percentage >= 100) {
359 d->hideArea->hide();
360 return;
361 }
362
363 d->hideArea->setBrush(hideColor);
364
365 qreal diff = d->hideBottom->rect().top() - d->hideTop->rect().bottom();
366 diff -= (diff * percentage) / 100;
367
368 QRectF rect = d->hideArea->rect();
369 rect.setTop(d->hideBottom->rect().top() - diff);
370
371 d->hideArea->setRect(rect);
372
373 d->hideArea->show();
374}
375
376// ------------------------------------------------------------------------
377void KSaneViewer::updateHighlight()
378{
379 if (d->selection->isVisible()) {
380 QRectF rect;
381 // Left
382 rect.setCoords(0, 0, d->selection->rect().left(), d->img->height());
383 d->hideLeft->setRect(rect);
384
385 // Right
386 rect.setCoords(d->selection->rect().right(),
387 0,
388 d->img->width(),
389 d->img->height());
390 d->hideRight->setRect(rect);
391
392 // Top
393 rect.setCoords(d->selection->rect().left(),
394 0,
395 d->selection->rect().right(),
396 d->selection->rect().top());
397 d->hideTop->setRect(rect);
398
399 // Bottom
400 rect.setCoords(d->selection->rect().left(),
401 d->selection->rect().bottom(),
402 d->selection->rect().right(),
403 d->img->height());
404 d->hideBottom->setRect(rect);
405
406 d->hideLeft->show();
407 d->hideRight->show();
408 d->hideTop->show();
409 d->hideBottom->show();
410 d->hideArea->hide();
411 } else {
412 d->hideLeft->hide();
413 d->hideRight->hide();
414 d->hideTop->hide();
415 d->hideBottom->hide();
416 d->hideArea->hide();
417 }
418}
419
420// ------------------------------------------------------------------------
421void KSaneViewer::clearHighlight()
422{
423 d->hideLeft->hide();
424 d->hideRight->hide();
425 d->hideTop->hide();
426 d->hideBottom->hide();
427 d->hideArea->hide();
428}
429
430// ------------------------------------------------------------------------
431void KSaneViewer::updateSelVisibility()
432{
433 if ((d->selection->rect().width() > 0.001) &&
434 (d->selection->rect().height() > 0.001) &&
435 ((d->img->width() - d->selection->rect().width() > 0.1) ||
436 (d->img->height() - d->selection->rect().height() > 0.1))) {
437 d->selection->setVisible(true);
438 } else {
439 d->selection->setVisible(false);
440 }
441 updateHighlight();
442}
443
444// ---- Return the saved selection list size + 1 if the selection is visible -
445int KSaneViewer::selListSize()
446{
447 if (d->selection->isVisible()) {
448 return (d->selectionList.size() + 1);
449 } else {
450 return d->selectionList.size();
451 }
452}
453
454// ---- First return the "saved" selection sthen the active selection -----------
455bool KSaneViewer::selectionAt(int index, float &tl_x, float &tl_y, float &br_x, float &br_y)
456{
457 if ((index < 0) || (index > d->selectionList.size())) {
458 activeSelection(tl_x, tl_y, br_x, br_y);
459 return false;
460 }
461 if (index == d->selectionList.size()) {
462 return activeSelection(tl_x, tl_y, br_x, br_y);
463 }
464
465 tl_x = d->selectionList[index]->rect().left() / d->img->width();
466 tl_y = d->selectionList[index]->rect().top() / d->img->height();
467 br_x = d->selectionList[index]->rect().right() / d->img->width();
468 br_y = d->selectionList[index]->rect().bottom() / d->img->height();
469 return true;
470}
471
472// ------------------------------------------------------------------------
473bool KSaneViewer::activeSelection(float &tl_x, float &tl_y, float &br_x, float &br_y)
474{
475 if (!d->selection->isVisible()) {
476 tl_x = 0.0;
477 tl_y = 0.0;
478 br_x = 1.0;
479 br_y = 1.0;
480 return true;
481 }
482
483 tl_x = d->selection->rect().left() / d->img->width();
484 tl_y = d->selection->rect().top() / d->img->height();
485 br_x = d->selection->rect().right() / d->img->width();
486 br_y = d->selection->rect().bottom() / d->img->height();
487
488 if ((tl_x == br_x) || (tl_y == br_y)) {
489 tl_x = 0.0;
490 tl_y = 0.0;
491 br_x = 1.0;
492 br_y = 1.0;
493 return false; // just precaution
494 }
495 return true;
496}
497
498// ------------------------------------------------------------------------
499void KSaneViewer::clearActiveSelection()
500{
501 d->selection->setRect(QRectF(0, 0, 0, 0));
502 d->selection->intersects(QPointF(100, 100)); // don't show the add sign
503 d->selection->setVisible(false);
504}
505
506// ------------------------------------------------------------------------
507void KSaneViewer::clearSavedSelections()
508{
509 // first remove any old saved selections
510 SelectionItem *tmp;
511 while (!d->selectionList.isEmpty()) {
512 tmp = d->selectionList.takeFirst();
513 d->scene->removeItem(tmp);
514 delete tmp;
515 }
516}
517
518// ------------------------------------------------------------------------
519void KSaneViewer::clearSelections()
520{
521 clearActiveSelection();
522 clearSavedSelections();
523 updateSelVisibility();
524}
525
526// ------------------------------------------------------------------------
527void KSaneViewer::setMultiselectionEnabled(bool enabled)
528{
529 d->multiSelectionEnabled = enabled;
530 clearSelections();
531 d->selection->setAddButtonEnabled(enabled);
532}
533
534
535// ------------------------------------------------------------------------
536void KSaneViewer::wheelEvent(QWheelEvent *e)
537{
538 if (e->modifiers() == Qt::ControlModifier) {
539 d->wheelDelta += e->angleDelta().y();
540
541 while (d->wheelDelta >= QWheelEvent::DefaultDeltasPerStep) {
542 zoomIn();
543 d->wheelDelta -= QWheelEvent::DefaultDeltasPerStep;
544 }
545
546 while (d->wheelDelta <= -QWheelEvent::DefaultDeltasPerStep) {
547 zoomOut();
548 d->wheelDelta += QWheelEvent::DefaultDeltasPerStep;
549 }
550 } else {
552 }
553}
554
555// ------------------------------------------------------------------------
556void KSaneViewer::mousePressEvent(QMouseEvent *e)
557{
558 if (e->button() == Qt::LeftButton) {
559 d->m_left_last_x = e->x();
560 d->m_left_last_y = e->y();
561 QPointF scenePoint = scenePos(e) * d->selection->devicePixelRatio();
562 d->lastSPoint = scenePoint;
563 if (e->modifiers() != Qt::ControlModifier) {
564 if (!d->selection->isVisible()) {
565 d->selection->setVisible(true);
566 d->selection->setRect(QRectF(scenePoint, QSizeF(0, 0)));
567 d->selection->intersects(scenePoint); // just to disable add/remove
568 d->change = SelectionItem::BottomRight;
569 } else if (d->selection->intersects(scenePoint) == SelectionItem::None) {
570 d->selection->setRect(QRectF(scenePoint, QSizeF(0, 0)));
571 d->change = SelectionItem::BottomRight;
572 }
573 updateHighlight();
574 }
575 }
577}
578
579// ------------------------------------------------------------------------
580void KSaneViewer::mouseReleaseEvent(QMouseEvent *e)
581{
582 bool removed = false;
583 if (e->button() == Qt::LeftButton) {
584 if ((d->selection->rect().width() < 0.001) ||
585 (d->selection->rect().height() < 0.001)) {
586 Q_EMIT newSelection(0.0, 0.0, 1.0, 1.0);
587 clearActiveSelection();
588 }
589
590 QPointF scenePoint = scenePos(e) * d->selection->devicePixelRatio();
591 for (int i = 0; i < d->selectionList.size(); i++) {
592 if (d->selectionList[i]->intersects(scenePoint) == SelectionItem::AddRemove) {
593 d->scene->removeItem(d->selectionList[i]);
594 SelectionItem *tmp = d->selectionList[i];
595 d->selectionList.removeAt(i);
596 d->selection->setVisible(true);
597 d->selection->setRect(tmp->rect());
598 d->selection->intersects(scenePoint); // just to enable add/remove
599 delete tmp;
600 removed = true;
601 break;
602 }
603 }
604 if (!removed && (d->selection->intersects(scenePoint) == SelectionItem::AddRemove)) {
605 // add the current selection
606 SelectionItem *tmp = new SelectionItem(d->selection->rect());
607 tmp->setDevicePixelRatio(d->img->devicePixelRatio());
608 d->selectionList.push_back(tmp);
609 d->selectionList.back()->setSaved(true);
610 d->selectionList.back()->saveZoom(transform().m11());
611 d->scene->addItem(d->selectionList.back());
612 d->selectionList.back()->setZValue(9);
613 d->selectionList.back()->intersects(scenePoint);
614
615 // clear the old one
616 Q_EMIT newSelection(0.0, 0.0, 1.0, 1.0);
617 clearActiveSelection();
618 }
619 }
620
621 if ((e->modifiers() != Qt::ControlModifier) &&
622 (d->selection->isVisible()) &&
623 (d->img->width() > 0.001) &&
624 (d->img->height() > 0.001)) {
625 float tlx = d->selection->rect().left() / d->img->width();
626 float tly = d->selection->rect().top() / d->img->height();
627 float brx = d->selection->rect().right() / d->img->width();
628 float bry = d->selection->rect().bottom() / d->img->height();
629
630 Q_EMIT newSelection(tlx, tly, brx, bry);
631 }
632 updateHighlight();
634}
635
636// ------------------------------------------------------------------------
637void KSaneViewer::mouseMoveEvent(QMouseEvent *e)
638{
639 QPointF scenePoint = scenePos(e) * d->selection->devicePixelRatio();
640
641 if (e->buttons()&Qt::LeftButton) {
642 if (e->modifiers() == Qt::ControlModifier) {
643 int dx = e->x() - d->m_left_last_x;
644 int dy = e->y() - d->m_left_last_y;
645 verticalScrollBar()->setValue(verticalScrollBar()->value() - dy);
646 horizontalScrollBar()->setValue(horizontalScrollBar()->value() - dx);
647 d->m_left_last_x = e->x();
648 d->m_left_last_y = e->y();
649 } else {
650 ensureVisible(QRectF(scenePoint, QSizeF(0, 0)), 1, 1);
651 QRectF rect = d->selection->rect();
652 switch (d->change) {
653 case SelectionItem::None:
654 // should not be here :)
655 break;
656 case SelectionItem::Top:
657 if (scenePoint.y() < rect.bottom()) {
658 rect.setTop(scenePoint.y());
659 } else {
660 d->change = SelectionItem::Bottom;
661 rect.setBottom(scenePoint.y());
662 }
663 break;
664 case SelectionItem::TopRight:
665 if (scenePoint.x() > rect.left()) {
666 rect.setRight(scenePoint.x());
667 } else {
668 rect.setLeft(scenePoint.x());
669 d->change = SelectionItem::TopLeft;
670 }
671 if (scenePoint.y() < rect.bottom()) {
672 rect.setTop(scenePoint.y());
673 } else {
674 rect.setBottom(scenePoint.y());
675 d->change = SelectionItem::BottomLeft;
676 } // FIXME arrow
677 break;
678 case SelectionItem::Right:
679 if (scenePoint.x() > rect.left()) {
680 rect.setRight(scenePoint.x());
681 } else {
682 rect.setLeft(scenePoint.x());
683 d->change = SelectionItem::Left;
684 }
685 break;
686 case SelectionItem::BottomRight:
687 if (scenePoint.x() > rect.left()) {
688 rect.setRight(scenePoint.x());
689 } else {
690 rect.setLeft(scenePoint.x());
691 d->change = SelectionItem::BottomLeft;
692 }
693 if (scenePoint.y() > rect.top()) {
694 rect.setBottom(scenePoint.y());
695 } else {
696 rect.setTop(scenePoint.y());
697 d->change = SelectionItem::TopRight;
698 } // FIXME arrow
699 break;
700 case SelectionItem::Bottom:
701 if (scenePoint.y() > rect.top()) {
702 rect.setBottom(scenePoint.y());
703 } else {
704 d->change = SelectionItem::Top;
705 rect.setTop(scenePoint.y());
706 }
707 break;
708 case SelectionItem::BottomLeft:
709 if (scenePoint.x() < rect.right()) {
710 rect.setLeft(scenePoint.x());
711 } else {
712 rect.setRight(scenePoint.x());
713 d->change = SelectionItem::BottomRight;
714 }
715 if (scenePoint.y() > rect.top()) {
716 rect.setBottom(scenePoint.y());
717 } else {
718 rect.setTop(scenePoint.y());
719 d->change = SelectionItem::TopLeft;
720 } // FIXME arrow
721 break;
722 case SelectionItem::Left:
723 if (scenePoint.x() < rect.right()) {
724 rect.setLeft(scenePoint.x());
725 } else {
726 rect.setRight(scenePoint.x());
727 d->change = SelectionItem::Right;
728 }
729 break;
730 case SelectionItem::TopLeft:
731 if (scenePoint.x() < rect.right()) {
732 rect.setLeft(scenePoint.x());
733 } else {
734 rect.setRight(scenePoint.x());
735 d->change = SelectionItem::TopRight;
736 }
737 if (scenePoint.y() < rect.bottom()) {
738 rect.setTop(scenePoint.y());
739 } else {
740 rect.setBottom(scenePoint.y());
741 d->change = SelectionItem::BottomLeft;
742 }// FIXME arrow
743 break;
744 case SelectionItem::Move:
745 rect.translate(d->selection->fixTranslation(scenePoint - d->lastSPoint));
746 break;
747 case SelectionItem::AddRemove:
748 // do nothing
749 break;
750 }
751 d->selection->setRect(rect);
752 }
753 } else if (d->selection->isVisible()) {
754 d->change = d->selection->intersects(scenePoint);
755
756 switch (d->change) {
757 case SelectionItem::None:
758 viewport()->setCursor(Qt::CrossCursor);
759 break;
760 case SelectionItem::Top:
761 viewport()->setCursor(Qt::SizeVerCursor);
762 break;
763 case SelectionItem::TopRight:
764 viewport()->setCursor(Qt::SizeBDiagCursor);
765 break;
766 case SelectionItem::Right:
767 viewport()->setCursor(Qt::SizeHorCursor);
768 break;
769 case SelectionItem::BottomRight:
770 viewport()->setCursor(Qt::SizeFDiagCursor);
771 break;
772 case SelectionItem::Bottom:
773 viewport()->setCursor(Qt::SizeVerCursor);
774 break;
775 case SelectionItem::BottomLeft:
776 viewport()->setCursor(Qt::SizeBDiagCursor);
777 break;
778 case SelectionItem::Left:
779 viewport()->setCursor(Qt::SizeHorCursor);
780 break;
781 case SelectionItem::TopLeft:
782 viewport()->setCursor(Qt::SizeFDiagCursor);
783 break;
784 case SelectionItem::Move:
785 viewport()->setCursor(Qt::SizeAllCursor);
786 break;
787 case SelectionItem::AddRemove:
788 viewport()->setCursor(Qt::ArrowCursor);
789 break;
790 }
791 } else {
792 viewport()->setCursor(Qt::CrossCursor);
793 }
794
795 // now check the selection list
796 for (int i = 0; i < d->selectionList.size(); i++) {
797 if (d->selectionList[i]->intersects(scenePoint) == SelectionItem::AddRemove) {
798 viewport()->setCursor(Qt::ArrowCursor);
799 }
800 }
801
802 d->lastSPoint = scenePoint;
803 updateHighlight();
805}
806
807// The change trigger before adding to the sum
808static const int DIFF_TRIGGER = 8;
809
810// The selection start/stop level trigger
811static const int SUM_TRIGGER = 4;
812
813// The selection start/stop level trigger for the floating average
814static const int AVERAGE_TRIGGER = 7;
815
816// The selection start/stop margin
817static const int SEL_MARGIN = 3;
818
819// Maximum number of allowed selections (this could be a settable variable)
820static const int MAX_NUM_SELECTIONS = 8;
821
822// floating average 'div' must be one less than 'count'
823static const int AVERAGE_COUNT = 50;
824static const int AVERAGE_MULT = 49;
825
826// Minimum selection area compared to the whole image
827static const float MIN_AREA_SIZE = 0.01;
828// ------------------------------------------------------------------------
829void KSaneViewer::findSelections(float area)
830{
831 // Reduce the size of the image to decrease noise and calculation time
832 float multiplier = sqrt(area / (d->img->height() * d->img->width()));
833
834 int width = (int)(d->img->width() * multiplier);
835 int height = (int)(d->img->height() * multiplier);
836
837 QImage img = d->img->scaled(width, height, Qt::KeepAspectRatio);
838 height = img.height(); // the size was probably not exact
839 width = img.width();
840
841 QVector<qint64> colSums(width + SEL_MARGIN + 1);
842 qint64 rowSum;
843 colSums.fill(0);
844 int pix;
845 int diff;
846 int hSelStart = -1;
847 int hSelEnd = -1;
848 int hSelMargin = 0;
849 int wSelStart = -1;
850 int wSelEnd = -1;
851 int wSelMargin = 0;
852
853 for (int h = 1; h < height; h++) {
854 rowSum = 0;
855 if (h < height - 1) {
856 // Special case for the left most pixel
857 pix = qGray(img.pixel(0, h));
858 diff = qAbs(pix - qGray(img.pixel(1, h)));
859 diff += qAbs(pix - qGray(img.pixel(0, h - 1)));
860 diff += qAbs(pix - qGray(img.pixel(0, h + 1)));
861 if (diff > DIFF_TRIGGER) {
862 colSums[0] += diff;
863 rowSum += diff;
864 }
865
866 // Special case for the right most pixel
867 pix = qGray(img.pixel(width - 1, h));
868 diff = qAbs(pix - qGray(img.pixel(width - 2, h)));
869 diff += qAbs(pix - qGray(img.pixel(width - 1, h - 1)));
870 diff += qAbs(pix - qGray(img.pixel(width - 1, h + 1)));
871 if (diff > DIFF_TRIGGER) {
872 colSums[width - 1] += diff;
873 rowSum += diff;
874 }
875
876 for (int w = 1; w < (width - 1); w++) {
877 pix = qGray(img.pixel(w, h));
878 diff = 0;
879 // how much does the pixel differ from the surrounding
880 diff += qAbs(pix - qGray(img.pixel(w - 1, h)));
881 diff += qAbs(pix - qGray(img.pixel(w + 1, h)));
882 diff += qAbs(pix - qGray(img.pixel(w, h - 1)));
883 diff += qAbs(pix - qGray(img.pixel(w, h + 1)));
884 if (diff > DIFF_TRIGGER) {
885 colSums[w] += diff;
886 rowSum += diff;
887 }
888 }
889 }
890
891 if ((rowSum / width) > SUM_TRIGGER) {
892 if (hSelStart < 0) {
893 if (hSelMargin < SEL_MARGIN) {
894 hSelMargin++;
895 }
896 if (hSelMargin == SEL_MARGIN) {
897 hSelStart = h - SEL_MARGIN + 1;
898 }
899 }
900 } else {
901 if (hSelStart >= 0) {
902 if (hSelMargin > 0) {
903 hSelMargin--;
904 }
905 }
906 if ((hSelStart > -1) && ((hSelMargin == 0) || (h == height - 1))) {
907 if (h == height - 1) {
908 hSelEnd = h - hSelMargin;
909 } else {
910 hSelEnd = h - SEL_MARGIN;
911 }
912 // We have the end of the vertical selection
913 // now figure out the horizontal part of the selection
914 for (int w = 0; w <= width; w++) { // colSums[width] will be 0
915 if ((colSums[w] / (h - hSelStart)) > SUM_TRIGGER) {
916 if (wSelStart < 0) {
917 if (wSelMargin < SEL_MARGIN) {
918 wSelMargin++;
919 }
920 if (wSelMargin == SEL_MARGIN) {
921 wSelStart = w - SEL_MARGIN + 1;
922 }
923 }
924 } else {
925 if (wSelStart >= 0) {
926 if (wSelMargin > 0) {
927 wSelMargin--;
928 }
929 }
930 if ((wSelStart >= 0) && ((wSelMargin == 0) || (w == width))) {
931 if (w == width) {
932 wSelEnd = width;
933 } else {
934 wSelEnd = w - SEL_MARGIN + 1;
935 }
936
937 // we have the end of a horizontal selection
938 if ((wSelEnd - wSelStart) < width) {
939 // skip selections that span the whole width
940 // calculate the coordinates in the original size
941 int x1 = wSelStart / multiplier;
942 int y1 = hSelStart / multiplier;
943 int x2 = wSelEnd / multiplier;
944 int y2 = hSelEnd / multiplier;
945 float selArea = (float)(wSelEnd - wSelStart) * (float)(hSelEnd - hSelStart);
946 if (selArea > (area * MIN_AREA_SIZE)) {
947 SelectionItem *tmp = new SelectionItem(QRect(QPoint(x1, y1), QPoint(x2, y2)));
948 tmp->setDevicePixelRatio(d->img->devicePixelRatio());
949 d->selectionList.push_back(tmp);
950 d->selectionList.back()->setSaved(true);
951 d->selectionList.back()->saveZoom(transform().m11());
952 d->scene->addItem(d->selectionList.back());
953 d->selectionList.back()->setZValue(9);
954 }
955 }
956 wSelStart = -1;
957 wSelEnd = -1;
958 (void)hSelEnd; // clang static analyzer report hSelEnd is never used.
959 wSelMargin = 0;
960 }
961 }
962 }
963 hSelStart = -1;
964 hSelEnd = -1;
965 hSelMargin = 0;
966 colSums.fill(0);
967 }
968 }
969 }
970
971 if (d->selectionList.size() > MAX_NUM_SELECTIONS) {
972 // smaller area or should we give up??
973 clearSavedSelections();
974 //findSelections(area/2);
975 // instead of trying to find probably broken selections just give up
976 // and do not force broken selections on the user.
977 } else {
978 // 1/multiplier is the error margin caused by the resolution reduction
979 refineSelections(qRound(1 / multiplier));
980 // check that the selections are big enough
981 float minArea = d->img->height() * d->img->width() * MIN_AREA_SIZE;
982
983 int i = 0;
984 while (i < d->selectionList.size()) {
985 if ((d->selectionList[i]->rect().width() * d->selectionList[i]->rect().height()) < minArea) {
986 d->scene->removeItem(d->selectionList[i]);
987 d->selectionList.removeAt(i);
988 } else {
989 i++;
990 }
991 }
992 }
993}
994
995QSize KSaneViewer::sizeHint() const
996{
997 return QSize(250, 300); // a sensible size for a scan preview
998}
999
1000void KSaneViewer::refineSelections(int pixelMargin)
1001{
1002 // The end result
1003 int hSelStart;
1004 int hSelEnd;
1005 int wSelStart;
1006 int wSelEnd;
1007
1008 for (int i = 0; i < d->selectionList.size(); i++) {
1009 QRectF selRect = d->selectionList.at(i)->rect();
1010
1011 // original values
1012 hSelStart = (int)selRect.top();
1013 hSelEnd = (int)selRect.bottom();
1014 wSelStart = (int)selRect.left();
1015 wSelEnd = (int)selRect.right();
1016
1017 // Top
1018 // Too long iteration should not be a problem since the loop should be interrupted by the limit
1019 hSelStart = refineRow(hSelStart - pixelMargin, hSelEnd, wSelStart, wSelEnd);
1020
1021 // Bottom (from the bottom up wards)
1022 hSelEnd = refineRow(hSelEnd + pixelMargin, hSelStart, wSelStart, wSelEnd);
1023
1024 // Left
1025 wSelStart = refineColumn(wSelStart - pixelMargin, wSelEnd, hSelStart, hSelEnd);
1026
1027 // Right
1028 wSelEnd = refineColumn(wSelEnd + pixelMargin, wSelStart, hSelStart, hSelEnd);
1029
1030 // Now update the selection
1031 d->selectionList.at(i)->setRect(QRectF(QPointF(wSelStart, hSelStart), QPointF(wSelEnd, hSelEnd)));
1032 }
1033}
1034
1035int KSaneViewer::refineRow(int fromRow, int toRow, int colStart, int colEnd)
1036{
1037 int pix;
1038 int diff;
1039 float rowTrigger;
1040 int row;
1041 int addSub = (fromRow < toRow) ? 1 : -1;
1042
1043 colStart -= 2; //add some margin
1044 colEnd += 2; //add some margin
1045
1046 if (colStart < 1) {
1047 colStart = 1;
1048 }
1049 if (colEnd >= d->img->width() - 1) {
1050 colEnd = d->img->width() - 2;
1051 }
1052
1053 if (fromRow < 1) {
1054 fromRow = 1;
1055 }
1056 if (fromRow >= d->img->height() - 1) {
1057 fromRow = d->img->height() - 2;
1058 }
1059
1060 if (toRow < 1) {
1061 toRow = 1;
1062 }
1063 if (toRow >= d->img->height() - 1) {
1064 toRow = d->img->height() - 2;
1065 }
1066
1067 row = fromRow;
1068 while (row != toRow) {
1069 rowTrigger = 0;
1070 for (int w = colStart; w < colEnd; w++) {
1071 diff = 0;
1072 pix = qGray(d->img->pixel(w, row));
1073 // how much does the pixel differ from the surrounding
1074 diff += qAbs(pix - qGray(d->img->pixel(w - 1, row)));
1075 diff += qAbs(pix - qGray(d->img->pixel(w + 1, row)));
1076 diff += qAbs(pix - qGray(d->img->pixel(w, row - 1)));
1077 diff += qAbs(pix - qGray(d->img->pixel(w, row + 1)));
1078 if (diff <= DIFF_TRIGGER) {
1079 diff = 0;
1080 }
1081
1082 rowTrigger = ((rowTrigger * AVERAGE_MULT) + diff) / AVERAGE_COUNT;
1083
1084 if (rowTrigger > AVERAGE_TRIGGER) {
1085 break;
1086 }
1087 }
1088
1089 if (rowTrigger > AVERAGE_TRIGGER) {
1090 // row == 1 _probably_ means that the selection should start from 0
1091 // but that can not be detected if we start from 1 => include one extra column
1092 if (row == 1) {
1093 row = 0;
1094 }
1095 if (row == (d->img->width() - 2)) {
1096 row = d->img->width();
1097 }
1098 return row;
1099 }
1100 row += addSub;
1101 }
1102 return row;
1103}
1104
1105int KSaneViewer::refineColumn(int fromCol, int toCol, int rowStart, int rowEnd)
1106{
1107 int pix;
1108 int diff;
1109 float colTrigger;
1110 int col;
1111 int count;
1112 int addSub = (fromCol < toCol) ? 1 : -1;
1113
1114 rowStart -= 2; //add some margin
1115 rowEnd += 2; //add some margin
1116
1117 if (rowStart < 1) {
1118 rowStart = 1;
1119 }
1120 if (rowEnd >= d->img->height() - 1) {
1121 rowEnd = d->img->height() - 2;
1122 }
1123
1124 if (fromCol < 1) {
1125 fromCol = 1;
1126 }
1127 if (fromCol >= d->img->width() - 1) {
1128 fromCol = d->img->width() - 2;
1129 }
1130
1131 if (toCol < 1) {
1132 toCol = 1;
1133 }
1134 if (toCol >= d->img->width() - 1) {
1135 toCol = d->img->width() - 2;
1136 }
1137
1138 col = fromCol;
1139 while (col != toCol) {
1140 colTrigger = 0;
1141 count = 0;
1142 for (int row = rowStart; row < rowEnd; row++) {
1143 count++;
1144 diff = 0;
1145 pix = qGray(d->img->pixel(col, row));
1146 // how much does the pixel differ from the surrounding
1147 diff += qAbs(pix - qGray(d->img->pixel(col - 1, row)));
1148 diff += qAbs(pix - qGray(d->img->pixel(col + 1, row)));
1149 diff += qAbs(pix - qGray(d->img->pixel(col, row - 1)));
1150 diff += qAbs(pix - qGray(d->img->pixel(col, row + 1)));
1151 if (diff <= DIFF_TRIGGER) {
1152 diff = 0;
1153 }
1154
1155 colTrigger = ((colTrigger * AVERAGE_MULT) + diff) / AVERAGE_COUNT;
1156
1157 if (colTrigger > AVERAGE_TRIGGER) {
1158 break;
1159 }
1160 }
1161
1162 if (colTrigger > AVERAGE_TRIGGER) {
1163 // col == 1 _probably_ means that the selection should start from 0
1164 // but that can not be detected if we start from 1 => include one extra column
1165 if (col == 1) {
1166 col = 0;
1167 }
1168 if (col == (d->img->width() - 2)) {
1169 col = d->img->width();
1170 }
1171 return col;
1172 }
1173 col += addSub;
1174 }
1175 return col;
1176}
1177
1178QPointF KSaneViewer::scenePos(QMouseEvent *e) const
1179{
1180 // QGraphicsView::mapToScene() maps only QPoints, but in highdpi mode we want
1181 // to deal with non-rounded coordinates, that's why QPainterPath wrapper is used.
1182 // QMouseEvent::localPos() currently returns a rounded QPointF
1183 // (https://codereview.qt-project.org/259785), so we have to extract a fractional
1184 // part from QMouseEvent::screenPos().
1185 const QPointF screenPos = e->screenPos();
1186 QPointF delta = screenPos - screenPos.toPoint();
1187 return mapToScene(QPainterPath(e->pos() + delta)).currentPosition();
1188}
1189
1190} // NameSpace KSaneIface
1191
1192#include "moc_ksaneviewer.cpp"
QString i18n(const char *text, const TYPE &arg...)
KDOCTOOLS_EXPORT QString transform(const QString &file, const QString &stylesheet, const QList< const char * > &params=QList< const char * >())
const QList< QKeySequence > & zoomIn()
const QList< QKeySequence > & zoomOut()
void triggered(bool checked)
virtual void mouseMoveEvent(QMouseEvent *event) override
virtual void mousePressEvent(QMouseEvent *event) override
virtual void mouseReleaseEvent(QMouseEvent *event) override
virtual void wheelEvent(QWheelEvent *event) override
QIcon fromTheme(const QString &name)
qreal devicePixelRatio() const const
int height() const const
QRgb pixel(const QPoint &position) const const
QImage scaled(const QSize &size, Qt::AspectRatioMode aspectRatioMode, Qt::TransformationMode transformMode) const const
int width() const const
Qt::KeyboardModifiers modifiers() const const
QPoint pos() const const
QPointF screenPos() const const
int x() const const
int y() const const
void drawImage(const QPoint &point, const QImage &image)
void fillRect(const QRect &rectangle, QGradient::Preset preset)
int y() const const
QPoint toPoint() const const
qreal x() const const
qreal y() const const
qreal bottom() const const
qreal left() const const
qreal right() const const
void setBottom(qreal y)
void setCoords(qreal x1, qreal y1, qreal x2, qreal y2)
void setLeft(qreal x)
void setRect(qreal x, qreal y, qreal width, qreal height)
void setRight(qreal x)
void setTop(qreal y)
QSizeF size() const const
qreal top() const const
QPointF topLeft() const const
void translate(const QPointF &offset)
Qt::MouseButton button() const const
Qt::MouseButtons buttons() const const
KeepAspectRatio
ActionsContextMenu
CrossCursor
ControlModifier
LeftButton
MiterJoin
SolidLine
QFuture< ArgsType< Signal > > connect(Sender *sender, Signal signal)
QPoint angleDelta() const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Fri Dec 6 2024 12:11:53 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.