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