1 /*
2  * LXImage-Qt - a simple and fast image viewer
3  * Copyright (C) 2013  PCMan <pcman.tw@gmail.com>
4  *
5  * This program is free software; you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation; either version 2 of the License, or
8  * (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License along
16  * with this program; if not, write to the Free Software Foundation, Inc.,
17  * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
18  *
19  */
20 
21 #include "imageview.h"
22 #include <QWheelEvent>
23 #include <QPaintEvent>
24 #include <QPainter>
25 #include <QTimer>
26 #include <QPolygon>
27 #include <QStyle>
28 #include <QLabel>
29 #include <QGraphicsProxyWidget>
30 #include <QGraphicsSvgItem>
31 #include <QStyleOptionGraphicsItem>
32 #include <QPainter>
33 #include <QPainterPath>
34 #include <QGuiApplication>
35 #include <QtMath>
36 
37 #define CURSOR_HIDE_DELY 3000
38 #define GRAY 127
39 
40 namespace LxImage {
41 
ImageView(QWidget * parent)42 ImageView::ImageView(QWidget* parent):
43   QGraphicsView(parent),
44   scene_(new GraphicsScene(this)),
45   imageItem_(new QGraphicsRectItem()),
46   outlineItem_(new QGraphicsRectItem()),
47   gifMovie_(nullptr),
48   cacheTimer_(nullptr),
49   cursorTimer_(nullptr),
50   scaleFactor_(1.0),
51   autoZoomFit_(false),
52   smoothOnZoom_(true),
53   isSVG(false),
54   currentTool(ToolNone),
55   nextNumber(1),
56   showOutline_(false) {
57 
58   setViewportMargins(0, 0, 0, 0);
59   setContentsMargins(0, 0, 0, 0);
60   setLineWidth(0);
61 
62   setScene(scene_);
63   connect(scene_, &GraphicsScene::fileDropped, this, &ImageView::onFileDropped);
64   imageItem_->hide();
65   imageItem_->setPen(QPen(Qt::NoPen)); // remove the border
66   scene_->addItem(imageItem_);
67 
68   outlineItem_->hide();
69   outlineItem_->setPen(QPen(Qt::NoPen));
70   scene_->addItem(outlineItem_);
71 }
72 
~ImageView()73 ImageView::~ImageView() {
74   scene_->clear(); // deletes all items
75   if(gifMovie_)
76     delete gifMovie_;
77   if(cacheTimer_) {
78     cacheTimer_->stop();
79     delete cacheTimer_;
80   }
81   if(cursorTimer_) {
82     cursorTimer_->stop();
83     delete cursorTimer_;
84   }
85 }
86 
imageGraphicsItem() const87 QGraphicsItem* ImageView::imageGraphicsItem() const {
88   if(!items().isEmpty()) {
89     return (items().constLast()); // the lowermost item
90   }
91   return nullptr;
92 }
93 
onFileDropped(const QString file)94 void ImageView::onFileDropped(const QString file) {
95     Q_EMIT fileDropped(file);
96 }
97 
wheelEvent(QWheelEvent * event)98 void ImageView::wheelEvent(QWheelEvent* event) {
99   QPoint angleDelta = event->angleDelta();
100   Qt::Orientation orient = (qAbs(angleDelta.x()) > qAbs(angleDelta.y()) ? Qt::Horizontal : Qt::Vertical);
101   int delta = (orient == Qt::Horizontal ? angleDelta.x() : angleDelta.y());
102   // Ctrl key is pressed
103   if(event->modifiers() & Qt::ControlModifier) {
104     if(delta > 0) { // forward
105       zoomIn();
106     }
107     else { // backward
108       zoomOut();
109     }
110   }
111   else {
112     // The default handler QGraphicsView::wheelEvent(event) tries to
113     // scroll the view, which is not what we need.
114     // Skip the default handler and use its parent QWidget's handler here.
115     QWidget::wheelEvent(event);
116   }
117 }
118 
mouseDoubleClickEvent(QMouseEvent * event)119 void ImageView::mouseDoubleClickEvent(QMouseEvent* event) {
120   // The default behaviour of QGraphicsView::mouseDoubleClickEvent() is
121   // not needed for us. We call its parent class instead so the event can be
122   // filtered by event filter installed on the view.
123   // QGraphicsView::mouseDoubleClickEvent(event);
124   QAbstractScrollArea::mouseDoubleClickEvent(event);
125 }
126 
mousePressEvent(QMouseEvent * event)127 void ImageView::mousePressEvent(QMouseEvent * event) {
128   if(currentTool == ToolNone) {
129     QGraphicsView::mousePressEvent(event);
130     if(cursorTimer_) {
131       cursorTimer_->stop();
132     }
133   }
134   else {
135     startPoint = mapToScene(event->pos()).toPoint();
136   }
137 }
138 
mouseReleaseEvent(QMouseEvent * event)139 void ImageView::mouseReleaseEvent(QMouseEvent* event) {
140   if(currentTool == ToolNone) {
141     QGraphicsView::mouseReleaseEvent(event);
142     if(cursorTimer_) {
143       cursorTimer_->start(CURSOR_HIDE_DELY);
144     }
145   }
146   else if(!image_.isNull()) {
147     QPoint endPoint = mapToScene(event->pos()).toPoint();
148 
149     QPainter painter(&image_);
150     painter.setRenderHint(QPainter::Antialiasing, true);
151     painter.setPen(QPen(Qt::red, 5));
152 
153     switch (currentTool) {
154     case ToolArrow:
155       drawArrow(painter, startPoint, endPoint, M_PI / 8, 25);
156       break;
157     case ToolRectangle:
158       // Draw the rectangle in the image and scene at the same time
159       painter.drawRect(QRect(startPoint, endPoint));
160       annotations.append(scene_->addRect(QRect(startPoint, endPoint), painter.pen()));
161       break;
162     case ToolCircle:
163       // Draw the circle in the image and scene at the same time
164       painter.drawEllipse(QRect(startPoint, endPoint));
165       annotations.append(scene_->addEllipse(QRect(startPoint, endPoint), painter.pen()));
166       break;
167     case ToolNumber:
168     {
169       // Set the font
170       QFont font;
171       font.setPixelSize(32);
172       painter.setFont(font);
173 
174       // Calculate the dimensions of the text
175       QString text = QStringLiteral("%1").arg(nextNumber++);
176       QRectF textRect = painter.boundingRect(image_.rect(), 0, text);
177       textRect.moveTo(endPoint);
178 
179       // Calculate the dimensions of the circle
180       qreal radius = qSqrt(textRect.width() * textRect.width() +
181                            textRect.height() * textRect.height()) / 2;
182       QRectF circleRect(textRect.left() + (textRect.width() / 2 - radius),
183                         textRect.top() + (textRect.height() / 2 - radius),
184                         radius * 2, radius * 2);
185 
186       // Draw the circle in the image
187       QPainterPath path;
188       path.addEllipse(circleRect);
189       painter.fillPath(path, Qt::red);
190       painter.drawPath(path);
191       // Draw the circle in the sence
192       auto item = scene_->addPath(path, painter.pen(), QBrush(Qt::red));
193       annotations.append(item);
194 
195       // Draw the text in the image
196       painter.setPen(Qt::white);
197       painter.drawText(textRect, Qt::AlignCenter, text);
198       // Add the text as a child of the circle
199       // NOTE: Not adding it directly to the scene is important with SVG/GIF transformations
200       QGraphicsSimpleTextItem* textItem = new QGraphicsSimpleTextItem(text, item);
201       textItem->setFont(font);
202       textItem->setBrush(Qt::white);
203       textItem->setPos(textRect.topLeft());
204 
205       break;
206     }
207     default:
208       break;
209     }
210     painter.end();
211     generateCache();
212   }
213 }
214 
mouseMoveEvent(QMouseEvent * event)215 void ImageView::mouseMoveEvent(QMouseEvent* event) {
216   QGraphicsView::mouseMoveEvent(event);
217   if(cursorTimer_
218      && (viewport()->cursor().shape() == Qt::BlankCursor
219          || viewport()->cursor().shape() == Qt::OpenHandCursor)) {
220     cursorTimer_->start(CURSOR_HIDE_DELY); // restart timer
221     viewport()->setCursor(Qt::OpenHandCursor);
222  }
223 }
224 
focusInEvent(QFocusEvent * event)225 void ImageView::focusInEvent(QFocusEvent* event) {
226   QGraphicsView::focusInEvent(event);
227   if(cursorTimer_
228      && (viewport()->cursor().shape() == Qt::BlankCursor
229          || viewport()->cursor().shape() == Qt::OpenHandCursor)) {
230     cursorTimer_->start(CURSOR_HIDE_DELY); // restart timer
231     viewport()->setCursor(Qt::OpenHandCursor);
232   }
233 }
234 
resizeEvent(QResizeEvent * event)235 void ImageView::resizeEvent(QResizeEvent* event) {
236   QGraphicsView::resizeEvent(event);
237   if(autoZoomFit_)
238     zoomFit();
239 }
240 
zoomFit()241 void ImageView::zoomFit() {
242   if(!image_.isNull()) {
243     // if the image is smaller than our view, use its original size
244     // instead of scaling it up.
245     if(static_cast<int>(image_.width() / qApp->devicePixelRatio()) <= width()
246        && static_cast<int>(image_.height() / qApp->devicePixelRatio()) <= height()) {
247       bool tmp = autoZoomFit_; // should be restored because it may be changed below
248       zoomOriginal();
249       autoZoomFit_ = tmp;
250       return;
251     }
252   }
253   fitInView(scene_->sceneRect(), Qt::KeepAspectRatio);
254   scaleFactor_ = transform().m11();
255   queueGenerateCache();
256 }
257 
zoomIn()258 void ImageView::zoomIn() {
259   autoZoomFit_ = false;
260   if(!image_.isNull()) {
261     resetTransform();
262     scaleFactor_ *= 1.1;
263     scale(scaleFactor_, scaleFactor_);
264     queueGenerateCache();
265     Q_EMIT zooming();
266   }
267 }
268 
zoomOut()269 void ImageView::zoomOut() {
270   autoZoomFit_ = false;
271   if(!image_.isNull()) {
272     resetTransform();
273     scaleFactor_ /= 1.1;
274     scale(scaleFactor_, scaleFactor_);
275     queueGenerateCache();
276     Q_EMIT zooming();
277   }
278 }
279 
zoomOriginal()280 void ImageView::zoomOriginal() {
281   resetTransform();
282   scaleFactor_ = 1.0;
283   autoZoomFit_ = false;
284   queueGenerateCache();
285 }
286 
rotateImage(bool clockwise)287 void ImageView::rotateImage(bool clockwise) {
288   if(gifMovie_ || isSVG) {
289     if(QGraphicsItem* imageItem = imageGraphicsItem()) {
290       QTransform transform;
291       if(clockwise) {
292         transform.translate(imageItem->sceneBoundingRect().height(), 0);
293         transform.rotate(90);
294       }
295       else {
296         transform.translate(0, imageItem->sceneBoundingRect().width());
297         transform.rotate(-90);
298       }
299       // we need to apply transformations in the reverse order
300       QTransform prevTrans = imageItem->transform();
301       imageItem->setTransform(transform, false);
302       imageItem->setTransform(prevTrans, true);
303       // apply transformations to the outline item too
304       if(outlineItem_) {
305         outlineItem_->setTransform(transform, false);
306         outlineItem_->setTransform(prevTrans, true);
307       }
308       // Since, in the case of SVG and GIF, annotations are not parts of the QImage and
309       // because they might have been added at any time, they need to be transformed
310       // by considering their previous transformations separately.
311       for(const auto& annotation : qAsConst(annotations)) {
312         prevTrans = annotation->transform();
313         annotation->setTransform(transform, false);
314         annotation->setTransform(prevTrans, true);
315       }
316     }
317   }
318   if(!image_.isNull()) {
319     QTransform transform;
320     transform.rotate(clockwise ? 90.0 : -90.0);
321     image_ = image_.transformed(transform, Qt::SmoothTransformation);
322     int tmp = nextNumber; // restore it (may be restet by setImage())
323     /* when this is GIF or SVG, we need to transform its corresponding QImage
324        without showing it to have right measures for auto-zooming and other things */
325     setImage(image_, !gifMovie_ && !isSVG);
326     nextNumber = tmp;
327   }
328 }
329 
flipImage(bool horizontal)330 void ImageView::flipImage(bool horizontal) {
331   if(gifMovie_ || isSVG) {
332     if(QGraphicsItem* imageItem = imageGraphicsItem()) {
333       QTransform transform;
334       if(horizontal) {
335         transform.scale(-1, 1);
336         transform.translate(-imageItem->sceneBoundingRect().width(), 0);
337       }
338       else {
339         transform.scale(1, -1);
340         transform.translate(0, -imageItem->sceneBoundingRect().height());
341       }
342       QTransform prevTrans = imageItem->transform();
343       imageItem->setTransform(transform, false);
344       imageItem->setTransform(prevTrans, true);
345       if(outlineItem_) {
346         outlineItem_->setTransform(transform, false);
347         outlineItem_->setTransform(prevTrans, true);
348       }
349       for(const auto& annotation : qAsConst(annotations)) {
350         prevTrans = annotation->transform();
351         annotation->setTransform(transform, false);
352         annotation->setTransform(prevTrans, true);
353       }
354     }
355   }
356   if(!image_.isNull()) {
357     if(horizontal) {
358       image_ = image_.mirrored(true, false);
359     }
360     else {
361       image_ = image_.mirrored(false, true);
362     }
363     int tmp = nextNumber;
364     setImage(image_, !gifMovie_ && !isSVG);
365     nextNumber = tmp;
366   }
367 }
368 
resizeImage(const QSize & newSize)369 bool ImageView::resizeImage(const QSize& newSize) {
370   QSize imgSize(image_.size());
371   if(newSize == imgSize) {
372     return false;
373   }
374   int tmp = nextNumber;
375   if(!isSVG) { // with SVG, we get a sharp image below
376     image_ = image_.scaled(newSize, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
377     setImage(image_, !gifMovie_);
378   }
379   if(gifMovie_ || isSVG) {
380     if(QGraphicsItem* imageItem = imageGraphicsItem()) {
381       qreal sx = static_cast<qreal>(newSize.width()) / imgSize.width();
382       qreal sy = static_cast<qreal>(newSize.height()) / imgSize.height();
383       QTransform transform;
384       transform.scale(sx, sy);
385       QTransform prevTrans = imageItem->transform();
386       imageItem->setTransform(transform, false);
387       imageItem->setTransform(prevTrans, true);
388       if(outlineItem_) {
389         outlineItem_->setTransform(transform, false);
390         outlineItem_->setTransform(prevTrans, true);
391       }
392       for(const auto& annotation : qAsConst(annotations)) {
393         prevTrans = annotation->transform();
394         annotation->setTransform(transform, false);
395         annotation->setTransform(prevTrans, true);
396       }
397 
398       if(isSVG) {
399         // create and set a sharp scaled image with SVG
400         QPixmap pixmap(newSize);
401         QPainter painter(&pixmap);
402         painter.setRenderHint(QPainter::Antialiasing);
403         painter.save();
404         painter.setTransform(imageItem->transform());
405         QStyleOptionGraphicsItem opt;
406         imageItem->paint(&painter, &opt);
407         painter.restore();
408         // draw annotations
409         for(const auto& annotation : qAsConst(annotations)) {
410           painter.save();
411           painter.setTransform(annotation->transform());
412           annotation->paint(&painter, &opt);
413           // also draw child annotations (numbers inside circles)
414           const auto children = annotation->childItems();
415           for(const auto& child : children) {
416             painter.save();
417             painter.translate(child->pos());
418             child->paint(&painter, &opt);
419             painter.restore();
420           }
421           painter.restore();
422         }
423         image_ = pixmap.toImage();
424         setImage(image_, false);
425       }
426     }
427   }
428   nextNumber = tmp;
429   return true;
430 }
431 
drawOutline()432 void ImageView::drawOutline() {
433   QColor col = QColor(Qt::black);
434   if(qGray(backgroundBrush().color().rgb()) < GRAY) {
435     col = QColor(Qt::white);
436   }
437   QPen outline(col, 1, Qt::DashLine);
438   outline.setCosmetic(true);
439   outlineItem_->setPen(outline);
440   outlineItem_->setBrush(Qt::NoBrush);
441   outlineItem_->setVisible(showOutline_);
442   outlineItem_->setZValue(1); // to be drawn on top of all other items
443 }
444 
setImage(const QImage & image,bool show)445 void ImageView::setImage(const QImage& image, bool show) {
446   if(show) {
447     resetView();
448     if(gifMovie_ || isSVG) { // a gif animation or SVG file was shown before
449       scene_->clear();
450       isSVG = false;
451       if(gifMovie_) { // should be deleted explicitly
452         delete gifMovie_;
453         gifMovie_ = nullptr;
454       }
455       // recreate the rect item
456       imageItem_ = new QGraphicsRectItem();
457       imageItem_->hide();
458       imageItem_->setPen(QPen(Qt::NoPen));
459       scene_->addItem(imageItem_);
460       // outline
461       outlineItem_ = new QGraphicsRectItem();
462       outlineItem_->hide();
463       outlineItem_->setPen(QPen(Qt::NoPen));
464       scene_->addItem(outlineItem_);
465     }
466   }
467 
468   image_ = image;
469   QRectF r(QPointF(0, 0), image_.size() / qApp->devicePixelRatio());
470   if(image.isNull()) {
471     imageItem_->hide();
472     imageItem_->setBrush(QBrush());
473     outlineItem_->hide();
474     outlineItem_->setBrush(QBrush());
475     scene_->setSceneRect(0, 0, 0, 0);
476   }
477   else {
478     if(show) {
479       image_.setDevicePixelRatio(qApp->devicePixelRatio());
480       imageItem_->setRect(r);
481       imageItem_->setBrush(image_);
482       imageItem_->show();
483       // outline
484       outlineItem_->setRect(r);
485       drawOutline();
486     }
487     scene_->setSceneRect(r);
488   }
489 
490   if(autoZoomFit_)
491     zoomFit();
492   queueGenerateCache();
493 }
494 
setGifAnimation(const QString & fileName)495 void ImageView::setGifAnimation(const QString& fileName) {
496   resetView();
497   /* the built-in gif reader gives the first frame, which won't
498      be shown but is used for tracking position and dimensions */
499   image_ = QImage(fileName);
500   if(image_.isNull()) {
501     if(imageItem_) {
502       imageItem_->hide();
503       imageItem_->setBrush(QBrush());
504     }
505     if(outlineItem_) {
506       outlineItem_->hide();
507       outlineItem_->setBrush(QBrush());
508     }
509     scene_->setSceneRect(0, 0, 0, 0);
510   }
511   else {
512     scene_->clear();
513     imageItem_ = nullptr; // it's deleted by clear();
514     if(gifMovie_) {
515       delete gifMovie_;
516       gifMovie_ = nullptr;
517     }
518     QPixmap pix(image_.size());
519     pix.setDevicePixelRatio(qApp->devicePixelRatio());
520     pix.fill(Qt::transparent);
521     QGraphicsItem* gifItem = new QGraphicsPixmapItem(pix);
522     QLabel* gifLabel = new QLabel();
523     gifLabel->setMaximumSize(pix.size() / qApp->devicePixelRatio()); // show gif with its real size
524     gifMovie_ = new QMovie(fileName);
525     QGraphicsProxyWidget* gifWidget = new QGraphicsProxyWidget(gifItem);
526     gifLabel->setAttribute(Qt::WA_NoSystemBackground);
527     gifLabel->setMovie(gifMovie_);
528     gifWidget->setWidget(gifLabel);
529     gifMovie_->start();
530     scene_->addItem(gifItem);
531     scene_->setSceneRect(gifItem->boundingRect());
532 
533     // outline
534     outlineItem_ = new QGraphicsRectItem(); // deleted by clear()
535     outlineItem_->setRect(gifItem->boundingRect());
536     drawOutline();
537     scene_->addItem(outlineItem_);
538   }
539 
540   if(autoZoomFit_)
541     zoomFit();
542   queueGenerateCache(); // deletes the cache timer in this case
543 }
544 
setSVG(const QString & fileName)545 void ImageView::setSVG(const QString& fileName) {
546   resetView();
547   image_ = QImage(fileName); // for tracking position and dimensions
548   if(image_.isNull()) {
549     if(imageItem_) {
550       imageItem_->hide();
551       imageItem_->setBrush(QBrush());
552     }
553     if(outlineItem_) {
554       outlineItem_->hide();
555       outlineItem_->setBrush(QBrush());
556     }
557     scene_->setSceneRect(0, 0, 0, 0);
558   }
559   else {
560     scene_->clear();
561     imageItem_ = nullptr;
562     isSVG = true;
563     QGraphicsSvgItem* svgItem = new QGraphicsSvgItem(fileName);
564     svgItem->setScale(1 / qApp->devicePixelRatio()); // show svg with its real size
565     scene_->addItem(svgItem);
566     QRectF r(svgItem->boundingRect());
567     r.setBottomRight(r.bottomRight() / qApp->devicePixelRatio());
568     r.setTopLeft(r.topLeft() / qApp->devicePixelRatio());
569     scene_->setSceneRect(r);
570 
571     // outline
572     outlineItem_ = new QGraphicsRectItem(); // deleted by clear()
573     outlineItem_->setRect(r);
574     drawOutline();
575     scene_->addItem(outlineItem_);
576   }
577 
578   if(autoZoomFit_)
579     zoomFit();
580   queueGenerateCache(); // deletes the cache timer in this case
581 }
582 
setScaleFactor(double factor)583 void ImageView::setScaleFactor(double factor) {
584   if(factor != scaleFactor_) {
585     scaleFactor_ = factor;
586     resetTransform();
587     scale(factor, factor);
588     queueGenerateCache();
589   }
590 }
591 
showOutline(bool show)592 void ImageView::showOutline(bool show) {
593   if(outlineItem_) {
594     outlineItem_->setVisible(show);
595     // the viewport may not be updated automatically
596     viewport()->update();
597   }
598   showOutline_ = show;
599 }
600 
updateOutline()601 void ImageView::updateOutline() {
602   if(outlineItem_) {
603     QColor col = QColor(Qt::black);
604     if(qGray(backgroundBrush().color().rgb()) < GRAY) {
605       col = QColor(Qt::white);
606     }
607     QPen outline = outlineItem_->pen();
608     outline.setColor(col);
609     outlineItem_->setPen(outline);
610     viewport()->update();
611   }
612 }
613 
paintEvent(QPaintEvent * event)614 void ImageView::paintEvent(QPaintEvent* event) {
615   if (!smoothOnZoom_) {
616     QGraphicsView::paintEvent(event);
617     return;
618   }
619   // if the image is scaled and we have a high quality cached image
620   if(imageItem_ && scaleFactor_ != 1.0 && !cachedPixmap_.isNull()) {
621     // rectangle of the whole image in viewport coordinate
622     QRect viewportImageRect = sceneToViewport(imageItem_->rect());
623     // the visible part of the image.
624     QRect desiredCachedRect = viewportToScene(viewportImageRect.intersected(viewport()->rect()));
625     // check if the cached area is what we need and if the cache is out of date
626     if(cachedSceneRect_ == desiredCachedRect) {
627       // rect of the image area that needs repaint, in viewport coordinate
628       QRect repaintImageRect = viewportImageRect.intersected(event->rect());
629       // see if the part asking for repaint is contained by our cache.
630       if(cachedRect_.contains(repaintImageRect)) {
631         QPainter painter(viewport());
632         painter.fillRect(event->rect(), backgroundBrush());
633         painter.drawPixmap(repaintImageRect, cachedPixmap_);
634         // outline
635         if(showOutline_) {
636             QColor col = QColor(Qt::black);
637             if(qGray(backgroundBrush().color().rgb()) < GRAY) {
638               col = QColor(Qt::white);
639             }
640             QPen outline(col, 1, Qt::DashLine);
641             painter.setPen(outline);
642             painter.drawRect(viewportImageRect);
643         }
644         return;
645       }
646     }
647   }
648   if(!image_.isNull()) { // we don't have a cache yet or it's out of date already, generate one
649     queueGenerateCache();
650   }
651   QGraphicsView::paintEvent(event);
652 }
653 
queueGenerateCache()654 void ImageView::queueGenerateCache() {
655   if(!cachedPixmap_.isNull()) // clear the old pixmap if there's any
656     cachedPixmap_ = QPixmap();
657 
658   // we don't need to cache the scaled image if its the same as the original image (scale:1.0)
659   // no cache for gif animations or SVG images either
660   if(scaleFactor_ == 1.0 || gifMovie_ || isSVG || !smoothOnZoom_) {
661     if(cacheTimer_) {
662       cacheTimer_->stop();
663       delete cacheTimer_;
664       cacheTimer_ = nullptr;
665     }
666     return;
667   }
668 
669   if(!cacheTimer_) {
670     cacheTimer_ = new QTimer();
671     cacheTimer_->setSingleShot(true);
672     connect(cacheTimer_, &QTimer::timeout, this, &ImageView::generateCache);
673   }
674   if(cacheTimer_)
675     cacheTimer_->start(200); // restart the timer
676 }
677 
678 // really generate the cache
generateCache()679 void ImageView::generateCache() {
680   // disable the one-shot timer
681   cacheTimer_->deleteLater();
682   cacheTimer_ = nullptr;
683 
684   if(!imageItem_ || image_.isNull()
685      || scaleFactor_ == 1.0 || gifMovie_ || isSVG || !smoothOnZoom_) {
686     return;
687   }
688 
689   // generate a cache for "the visible part" of the scaled image
690   // rectangle of the whole image in viewport coordinate
691   QRect viewportImageRect = sceneToViewport(imageItem_->rect());
692   // rect of the image area that's visible in the viewport (in viewport coordinate)
693   cachedRect_ = viewportImageRect.intersected(viewport()->rect());
694 
695   // convert to the coordinate of the original image
696   cachedSceneRect_ = viewportToScene(cachedRect_);
697   // create a sub image of the visible without real data copy
698   // Reference: https://stackoverflow.com/questions/12681554/dividing-qimage-to-smaller-pieces
699   QRect subRect = image_.rect().intersected(cachedSceneRect_);
700   const uchar* bits = image_.constBits();
701   unsigned int offset = subRect.x() * image_.depth() / 8 + subRect.y() * image_.bytesPerLine();
702   QImage subImage = QImage(bits + offset, subRect.width(), subRect.height(), image_.bytesPerLine(), image_.format());
703 
704   // If the original image has a color table, also use it for the subImage
705   QVector<QRgb> colorTable = image_.colorTable();
706   if(!colorTable.empty()) {
707     subImage.setColorTable(colorTable);
708   }
709 
710   // QImage scaled = subImage.scaled(subRect.width() * scaleFactor_, subRect.height() * scaleFactor_, Qt::KeepAspectRatio, Qt::SmoothTransformation);
711   QImage scaled = subImage.scaled(cachedRect_.size() * qApp->devicePixelRatio(), Qt::KeepAspectRatio, Qt::SmoothTransformation);
712 
713   // convert the cached scaled image to pixmap
714   cachedPixmap_ = QPixmap::fromImage(scaled);
715   viewport()->update();
716 }
717 
718 // convert viewport coordinate to the original image (not scaled).
viewportToScene(const QRect & rect)719 QRect ImageView::viewportToScene(const QRect& rect) {
720   // QPolygon poly = mapToScene(imageItem_->rect());
721   /* NOTE: The scene rectangle is shrunken by qApp->devicePixelRatio()
722      but we want the coordinates with respect to the original image. */
723   QPoint topLeft = (mapToScene(rect.topLeft()) * qApp->devicePixelRatio()).toPoint();
724   QPoint bottomRight = (mapToScene(rect.bottomRight()) * qApp->devicePixelRatio()).toPoint();
725   return QRect(topLeft, bottomRight);
726 }
727 
sceneToViewport(const QRectF & rect)728 QRect ImageView::sceneToViewport(const QRectF& rect) {
729   QPoint topLeft = mapFromScene(rect.topLeft());
730   QPoint bottomRight = mapFromScene(rect.bottomRight());
731   return QRect(topLeft, bottomRight);
732 }
733 
blankCursor()734 void ImageView::blankCursor() {
735   viewport()->setCursor(Qt::BlankCursor);
736 }
737 
hideCursor(bool enable)738 void ImageView::hideCursor(bool enable) {
739   if(enable) {
740     delete cursorTimer_;
741     cursorTimer_ = new QTimer(this);
742     cursorTimer_->setSingleShot(true);
743     connect(cursorTimer_, &QTimer::timeout, this, &ImageView::blankCursor);
744     if(viewport()->cursor().shape() == Qt::OpenHandCursor) {
745       cursorTimer_->start(CURSOR_HIDE_DELY);
746     }
747   }
748   else if(cursorTimer_) {
749     cursorTimer_->stop();
750     delete cursorTimer_;
751     cursorTimer_ = nullptr;
752     if(viewport()->cursor().shape() == Qt::BlankCursor) {
753       viewport()->setCursor(Qt::OpenHandCursor);
754     }
755   }
756 }
757 
activateTool(Tool tool)758 void ImageView::activateTool(Tool tool) {
759   currentTool = tool;
760   viewport()->setCursor(tool == ToolNone ?
761                             Qt::OpenHandCursor :
762                             Qt::CrossCursor);
763 }
764 
drawArrow(QPainter & painter,const QPoint & start,const QPoint & end,qreal tipAngle,int tipLen)765 void ImageView::drawArrow(QPainter &painter,
766                           const QPoint &start,
767                           const QPoint &end,
768                           qreal tipAngle,
769                           int tipLen)
770 {
771   // Draw the line in the inmage
772   painter.drawLine(start, end);
773   // Draw the line in the scene
774   annotations.append(scene_->addLine(QLine(start, end), painter.pen()));
775 
776   // Calculate the angle of the line
777   QPoint delta = end - start;
778   qreal angle = qAtan2(-delta.y(), delta.x()) - M_PI / 2;
779 
780   // Calculate the points of the lines that converge at the tip
781   QPoint tip1(
782     static_cast<int>(qSin(angle + tipAngle) * tipLen),
783     static_cast<int>(qCos(angle + tipAngle) * tipLen)
784   );
785   QPoint tip2(
786     static_cast<int>(qSin(angle - tipAngle) * tipLen),
787     static_cast<int>(qCos(angle - tipAngle) * tipLen)
788   );
789 
790   // Draw the two lines in the image
791   painter.drawLine(end, end + tip1);
792   painter.drawLine(end, end + tip2);
793   // Draw the two lines in the scene
794   annotations.append(scene_->addLine(QLine(end, end+tip1), painter.pen()));
795   annotations.append(scene_->addLine(QLine(end, end+tip2), painter.pen()));
796 }
797 
resetView()798 void ImageView::resetView() {
799   // reset transformation
800   if(QGraphicsItem* imageItem = imageGraphicsItem()) {
801     imageItem->resetTransform();
802     if(outlineItem_) {
803       outlineItem_->resetTransform();
804     }
805   }
806   // remove annotations
807   if(!annotations.isEmpty()) {
808     if(!scene_->items().isEmpty()) { // WARNING: This is not enough to guard against dangling pointers.
809       for(const auto& annotation : qAsConst(annotations)) {
810         scene_->removeItem(annotation);
811       }
812       qDeleteAll(annotations.begin(), annotations.end());
813     }
814     annotations.clear();
815   }
816   // reset numbering
817   nextNumber = 1;
818 }
819 
820 } // namespace LxImage
821