Pencil2D Animation
Download Community News Docs Contribute
  • Overview
  • Articles
  • Code
  •  
  • Class List
  • Class Index
  • Class Hierarchy
  • Class Members
  • File List
Loading...
Searching...
No Matches
  • core_lib
  • src
  • graphics
  • bitmap
bitmapimage.cpp
1/*
2
3Pencil2D - Traditional Animation Software
4Copyright (C) 2005-2007 Patrick Corrieri & Pascal Naidon
5Copyright (C) 2012-2020 Matthew Chiawen Chang
6
7This program is free software; you can redistribute it and/or
8modify it under the terms of the GNU General Public License
9as published by the Free Software Foundation; version 2 of the License.
10
11This program is distributed in the hope that it will be useful,
12but WITHOUT ANY WARRANTY; without even the implied warranty of
13MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14GNU General Public License for more details.
15
16*/
17#include "bitmapimage.h"
18
19#include <cmath>
20#include <QDebug>
21#include <QDir>
22#include <QFile>
23#include <QFileInfo>
24#include <QPainterPath>
25#include "util.h"
26
27#include "blitrect.h"
28#include "tile.h"
29#include "tiledbuffer.h"
30
31BitmapImage::BitmapImage()
32{
33}
34
35BitmapImage::BitmapImage(const BitmapImage& a) : KeyFrame(a)
36{
37 mBounds = a.mBounds;
38 mMinBound = a.mMinBound;
39 mEnableAutoCrop = a.mEnableAutoCrop;
40 mOpacity = a.mOpacity;
41 mImage = a.mImage;
42}
43
44BitmapImage::BitmapImage(const QRect& rectangle, const QColor& color)
45{
46 mBounds = rectangle;
47 mImage = QImage(mBounds.size(), QImage::Format_ARGB32_Premultiplied);
48 mImage.fill(color.rgba());
49 mMinBound = false;
50}
51
52BitmapImage::BitmapImage(const QPoint& topLeft, const QImage& image)
53{
54 mBounds = QRect(topLeft, image.size());
55 mMinBound = true;
56 mImage = image.convertToFormat(QImage::Format_ARGB32_Premultiplied);
57}
58
59BitmapImage::BitmapImage(const QPoint& topLeft, const QString& path)
60{
61 setFileName(path);
62 mImage = QImage();
63
64 mBounds = QRect(topLeft, QSize(-1, 0));
65 mMinBound = true;
66 setModified(false);
67}
68
69BitmapImage::~BitmapImage()
70{
71}
72
73void BitmapImage::setImage(QImage* img)
74{
75 Q_ASSERT(img && img->format() == QImage::Format_ARGB32_Premultiplied);
76 mImage = *img;
77 mMinBound = false;
78
79 modification();
80}
81
82BitmapImage& BitmapImage::operator=(const BitmapImage& a)
83{
84 if (this == &a)
85 {
86 return *this; // a self-assignment
87 }
88
89 KeyFrame::operator=(a);
90 mBounds = a.mBounds;
91 mMinBound = a.mMinBound;
92 mOpacity = a.mOpacity;
93 mImage = a.mImage;
94 modification();
95 return *this;
96}
97
98BitmapImage* BitmapImage::clone() const
99{
100 BitmapImage* b = new BitmapImage(*this);
101 b->setFileName(""); // don't link to the file of the source bitmap image
102
103 const bool validKeyFrame = !fileName().isEmpty();
104 if (validKeyFrame && !isModified())
105 {
106 // This bitmapImage is temporarily unloaded.
107 // since it's not in the memory, we need to copy the linked png file to prevent data loss.
108 QFileInfo finfo(fileName());
109 Q_ASSERT(finfo.isAbsolute());
110 Q_ASSERT(QFile::exists(fileName()));
111
112 QString newFilePath;
113 do
114 {
115 newFilePath = QString("%1/temp-%2.%3")
116 .arg(finfo.canonicalPath())
117 .arg(uniqueString(12))
118 .arg(finfo.suffix());
119 }
120 while (QFile::exists(newFilePath));
121
122 b->setFileName(newFilePath);
123 bool ok = QFile::copy(fileName(), newFilePath);
124 Q_ASSERT(ok);
125 qDebug() << "COPY>" << fileName();
126 }
127 return b;
128}
129
130void BitmapImage::loadFile()
131{
132 if (!fileName().isEmpty() && !isLoaded())
133 {
134 mImage = QImage(fileName()).convertToFormat(QImage::Format_ARGB32_Premultiplied);
135 mBounds.setSize(mImage.size());
136 mMinBound = false;
137 }
138}
139
140void BitmapImage::unloadFile()
141{
142 if (isModified() == false)
143 {
144 mImage = QImage();
145 }
146}
147
148bool BitmapImage::isLoaded() const
149{
150 return mImage.width() == mBounds.width();
151}
152
153quint64 BitmapImage::memoryUsage()
154{
155 if (!mImage.isNull())
156 {
157 return imageSize(mImage);
158 }
159 return 0;
160}
161
162void BitmapImage::paintImage(QPainter& painter)
163{
164 painter.drawImage(mBounds.topLeft(), *image());
165}
166
167void BitmapImage::paintImage(QPainter& painter, QImage& image, QRect sourceRect, QRect destRect)
168{
169 painter.drawImage(QRect(mBounds.topLeft(), destRect.size()),
170 image,
171 sourceRect);
172}
173
174QImage* BitmapImage::image()
175{
176 loadFile();
177 return &mImage;
178}
179
180BitmapImage BitmapImage::copy()
181{
182 return BitmapImage(mBounds.topLeft(), *image());
183}
184
185BitmapImage BitmapImage::copy(QRect rectangle)
186{
187 if (rectangle.isEmpty() || mBounds.isEmpty()) return BitmapImage();
188
189 QRect intersection2 = rectangle.translated(-mBounds.topLeft());
190
191 BitmapImage result(rectangle.topLeft(), image()->copy(intersection2));
192 return result;
193}
194
195void BitmapImage::paste(BitmapImage* bitmapImage, QPainter::CompositionMode cm)
196{
197 if(bitmapImage->width() <= 0 || bitmapImage->height() <= 0)
198 {
199 return;
200 }
201
202 setCompositionModeBounds(bitmapImage, cm);
203
204 QImage* image2 = bitmapImage->image();
205
206 QPainter painter(image());
207 painter.setCompositionMode(cm);
208 painter.drawImage(bitmapImage->mBounds.topLeft() - mBounds.topLeft(), *image2);
209 painter.end();
210
211 modification();
212}
213
214void BitmapImage::paste(const TiledBuffer* tiledBuffer, QPainter::CompositionMode cm)
215{
216 if(tiledBuffer->bounds().width() <= 0 || tiledBuffer->bounds().height() <= 0)
217 {
218 return;
219 }
220 extend(tiledBuffer->bounds());
221
222 QPainter painter(image());
223
224 painter.setCompositionMode(cm);
225 auto const tiles = tiledBuffer->tiles();
226 for (const Tile* item : tiles) {
227 const QPixmap& tilePixmap = item->pixmap();
228 const QPoint& tilePos = item->pos();
229 painter.drawPixmap(tilePos-mBounds.topLeft(), tilePixmap);
230 }
231 painter.end();
232
233 modification();
234}
235
236void BitmapImage::moveTopLeft(QPoint point)
237{
238 mBounds.moveTopLeft(point);
239 // Size is unchanged so there is no need to update mBounds
240 modification();
241}
242
243void BitmapImage::transform(QRect newBoundaries, bool smoothTransform)
244{
245 mBounds = newBoundaries;
246 newBoundaries.moveTopLeft(QPoint(0, 0));
247 QImage newImage(mBounds.size(), QImage::Format_ARGB32_Premultiplied);
248
249 QPainter painter(&newImage);
250 painter.setRenderHint(QPainter::SmoothPixmapTransform, smoothTransform);
251 painter.setCompositionMode(QPainter::CompositionMode_Source);
252 painter.fillRect(newImage.rect(), QColor(0, 0, 0, 0));
253 painter.setCompositionMode(QPainter::CompositionMode_SourceOver);
254 painter.drawImage(newBoundaries, *image());
255 painter.end();
256 mImage = newImage;
257
258 modification();
259}
260
261BitmapImage BitmapImage::transformed(QRect selection, QTransform transform, bool smoothTransform)
262{
263 Q_ASSERT(!selection.isEmpty());
264
265 BitmapImage selectedPart = copy(selection);
266
267 // Get the transformed image
268 QImage transformedImage;
269 if (smoothTransform)
270 {
271 transformedImage = selectedPart.image()->transformed(transform, Qt::SmoothTransformation);
272 }
273 else
274 {
275 transformedImage = selectedPart.image()->transformed(transform);
276 }
277 return BitmapImage(transform.mapRect(selection).normalized().topLeft(), transformedImage);
278}
279
280BitmapImage BitmapImage::transformed(QRect newBoundaries, bool smoothTransform)
281{
282 BitmapImage transformedImage(newBoundaries, QColor(0, 0, 0, 0));
283 QPainter painter(transformedImage.image());
284 painter.setRenderHint(QPainter::SmoothPixmapTransform, smoothTransform);
285 newBoundaries.moveTopLeft(QPoint(0, 0));
286 painter.drawImage(newBoundaries, *image());
287 painter.end();
288 return transformedImage;
289}
290
298void BitmapImage::updateBounds(QRect newBoundaries)
299{
300 // Check to make sure changes actually need to be made
301 if (mBounds == newBoundaries) return;
302
303 QImage newImage(newBoundaries.size(), QImage::Format_ARGB32_Premultiplied);
304 newImage.fill(Qt::transparent);
305 if (!newImage.isNull())
306 {
307 QPainter painter(&newImage);
308 painter.drawImage(mBounds.topLeft() - newBoundaries.topLeft(), mImage);
309 painter.end();
310 }
311 mImage = newImage;
312 mBounds = newBoundaries;
313 mMinBound = false;
314
315 modification();
316}
317
318void BitmapImage::extend(const QPoint &p)
319{
320 if (!mBounds.contains(p))
321 {
322 extend(QRect(p, QSize(1, 1)));
323 }
324}
325
326void BitmapImage::extend(QRect rectangle)
327{
328 if (rectangle.width() <= 0) rectangle.setWidth(1);
329 if (rectangle.height() <= 0) rectangle.setHeight(1);
330 if (mBounds.contains(rectangle))
331 {
332 // Do nothing
333 }
334 else
335 {
336 QRect newBoundaries = mBounds.united(rectangle).normalized();
337 QImage newImage(newBoundaries.size(), QImage::Format_ARGB32_Premultiplied);
338 newImage.fill(Qt::transparent);
339 if (!newImage.isNull())
340 {
341 QPainter painter(&newImage);
342 painter.drawImage(mBounds.topLeft() - newBoundaries.topLeft(), *image());
343 painter.end();
344 }
345 mImage = newImage;
346 mBounds = newBoundaries;
347
348 modification();
349 }
350}
351
359void BitmapImage::setCompositionModeBounds(BitmapImage *source, QPainter::CompositionMode cm)
360{
361 if (source)
362 {
363 setCompositionModeBounds(source->mBounds, source->mMinBound, cm);
364 }
365}
366
386void BitmapImage::setCompositionModeBounds(QRect sourceBounds, bool isSourceMinBounds, QPainter::CompositionMode cm)
387{
388 QRect newBoundaries;
389 switch(cm)
390 {
391 case QPainter::CompositionMode_Destination:
392 case QPainter::CompositionMode_SourceAtop:
393 // The Destination and SourceAtop modes
394 // do not change the bounds from destination.
395 newBoundaries = mBounds;
396 // mMinBound remains the same
397 break;
398 case QPainter::CompositionMode_SourceIn:
399 case QPainter::CompositionMode_DestinationIn:
400 case QPainter::CompositionMode_Clear:
401 case QPainter::CompositionMode_DestinationOut:
402 // The bounds of the result of SourceIn, DestinationIn, Clear, and DestinationOut
403 // modes are no larger than the destination bounds
404 newBoundaries = mBounds;
405 mMinBound = false;
406 break;
407 default:
408 // If it's not one of the above cases, create a union of the two bounds.
409 // This contains the minimum bounds, if both the destination and source
410 // use their respective minimum bounds.
411 newBoundaries = mBounds.united(sourceBounds);
412 mMinBound = mMinBound && isSourceMinBounds;
413 }
414
415 updateBounds(newBoundaries);
416}
417
431void BitmapImage::autoCrop()
432{
433 if (!mEnableAutoCrop) return;
434 if (mBounds.isEmpty()) return; // Exit if current bounds are null
435 if (mImage.isNull()) return;
436
437 Q_ASSERT(mBounds.size() == mImage.size());
438
439 // Exit if already min bounded
440 if (mMinBound) return;
441
442 // Get image properties
443 const int width = mImage.width();
444
445 // Relative top and bottom row indices (inclusive)
446 int relTop = 0;
447 int relBottom = mBounds.height() - 1;
448
449 // Check top row
450 bool isEmpty = true; // Used to track if a non-transparent pixel has been found
451 while (isEmpty && relTop <= relBottom) // Loop through rows
452 {
453 // Point cursor to the first pixel in the current top row
454 const QRgb* cursor = reinterpret_cast<const QRgb*>(mImage.constScanLine(relTop));
455 for (int col = 0; col < width; col++) // Loop through pixels in row
456 {
457 // If the pixel is not transparent
458 // (i.e. alpha channel > 0)
459 if (qAlpha(*cursor) != 0)
460 {
461 // We've found a non-transparent pixel in row relTop,
462 // so we can stop looking for one
463 isEmpty = false;
464 break;
465 }
466 // Move cursor to point to the next pixel in the row
467 cursor++;
468 }
469 if (isEmpty)
470 {
471 // If the row we just checked was empty, increase relTop
472 // to remove the empty row from the top of the bounding box
473 ++relTop;
474 }
475 }
476
477 // Check bottom row
478 isEmpty = true; // Reset isEmpty
479 while (isEmpty && relBottom >= relTop) // Loop through rows
480 {
481 // Point cursor to the first pixel in the current bottom row
482 const QRgb* cursor = reinterpret_cast<const QRgb*>(mImage.constScanLine(relBottom));
483 for (int col = 0; col < width; col++) // Loop through pixels in row
484 {
485 // If the pixel is not transparent
486 // (i.e. alpha channel > 0)
487 if(qAlpha(*cursor) != 0)
488 {
489 // We've found a non-transparent pixel in row relBottom,
490 // so we can stop looking for one
491 isEmpty = false;
492 break;
493 }
494 // Move cursor to point to the next pixel in the row
495 ++cursor;
496 }
497 if (isEmpty)
498 {
499 // If the row we just checked was empty, decrease relBottom
500 // to remove the empty row from the bottom of the bounding box
501 --relBottom;
502 }
503 }
504
505 // Relative left and right column indices (inclusive)
506 int relLeft = 0;
507 int relRight = mBounds.width()-1;
508
509 // Check left row
510 isEmpty = (relBottom >= relTop); // Check left only when
511 while (isEmpty && relBottom >= relTop && relLeft <= relRight) // Loop through columns
512 {
513 // Point cursor to the pixel at row relTop and column relLeft
514 const QRgb* cursor = reinterpret_cast<const QRgb*>(mImage.constScanLine(relTop)) + relLeft;
515 // Loop through pixels in column
516 // Note: we only need to loop from relTop to relBottom (inclusive)
517 // not the full image height, because rows 0 to relTop-1 and
518 // relBottom+1 to mBounds.height() have already been
519 // confirmed to contain only transparent pixels
520 for (int row = relTop; row <= relBottom; row++)
521 {
522 // If the pixel is not transparent
523 // (i.e. alpha channel > 0)
524 if(qAlpha(*cursor) != 0)
525 {
526 // We've found a non-transparent pixel in column relLeft,
527 // so we can stop looking for one
528 isEmpty = false;
529 break;
530 }
531 // Move cursor to point to next pixel in the column
532 // Increment by width because the data is in row-major order
533 cursor += width;
534 }
535 if (isEmpty)
536 {
537 // If the column we just checked was empty, increase relLeft
538 // to remove the empty column from the left of the bounding box
539 ++relLeft;
540 }
541 }
542
543 // Check right row
544 isEmpty = (relBottom >= relTop); // Reset isEmpty
545 while (isEmpty && relRight >= relLeft) // Loop through columns
546 {
547 // Point cursor to the pixel at row relTop and column relRight
548 const QRgb* cursor = reinterpret_cast<const QRgb*>(mImage.constScanLine(relTop)) + relRight;
549 // Loop through pixels in column
550 // Note: we only need to loop from relTop to relBottom (inclusive)
551 // not the full image height, because rows 0 to relTop-1 and
552 // relBottom+1 to mBounds.height()-1 have already been
553 // confirmed to contain only transparent pixels
554 for (int row = relTop; row <= relBottom; row++)
555 {
556 // If the pixel is not transparent
557 // (i.e. alpha channel > 0)
558 if(qAlpha(*cursor) != 0)
559 {
560 // We've found a non-transparent pixel in column relRight,
561 // so we can stop looking for one
562 isEmpty = false;
563 break;
564 }
565 // Move cursor to point to next pixel in the column
566 // Increment by width because the data is in row-major order
567 cursor += width;
568 }
569 if (isEmpty)
570 {
571 // If the column we just checked was empty, increase relRight
572 // to remove the empty column from the left of the bounding box
573 --relRight;
574 }
575 }
576
577 //qDebug() << "Original" << mBounds;
578 //qDebug() << "Autocrop" << relLeft << relTop << relRight - mBounds.width() + 1 << relBottom - mBounds.height() + 1;
579 // Update mBounds and mImage if necessary
580 updateBounds(mBounds.adjusted(relLeft, relTop, relRight - mBounds.width() + 1, relBottom - mBounds.height() + 1));
581
582 //qDebug() << "New bounds" << mBounds;
583
584 mMinBound = true;
585}
586
587QRgb BitmapImage::pixel(int x, int y)
588{
589 return pixel(QPoint(x, y));
590}
591
592QRgb BitmapImage::pixel(QPoint p)
593{
594 QRgb result = qRgba(0, 0, 0, 0); // black
595 if (mBounds.contains(p))
596 result = image()->pixel(p - mBounds.topLeft());
597 return result;
598}
599
600void BitmapImage::setPixel(int x, int y, QRgb color)
601{
602 setPixel(QPoint(x, y), color);
603}
604
605void BitmapImage::setPixel(QPoint p, QRgb color)
606{
607 setCompositionModeBounds(QRect(p, QSize(1,1)), true, QPainter::CompositionMode_SourceOver);
608 if (mBounds.contains(p))
609 {
610 image()->setPixel(p - mBounds.topLeft(), color);
611 }
612 modification();
613}
614
615void BitmapImage::fillNonAlphaPixels(const QRgb color)
616{
617 if (mBounds.isEmpty()) { return; }
618
619 BitmapImage fill(bounds(), color);
620 paste(&fill, QPainter::CompositionMode_SourceIn);
621}
622
623void BitmapImage::drawLine(QPointF P1, QPointF P2, QPen pen, QPainter::CompositionMode cm, bool antialiasing)
624{
625 int width = 2 + pen.width();
626 setCompositionModeBounds(QRect(P1.toPoint(), P2.toPoint()).normalized().adjusted(-width, -width, width, width), true, cm);
627 if (!image()->isNull())
628 {
629 QPainter painter(image());
630 painter.setCompositionMode(cm);
631 painter.setRenderHint(QPainter::Antialiasing, antialiasing);
632 painter.setPen(pen);
633 painter.drawLine(P1 - mBounds.topLeft(), P2 - mBounds.topLeft());
634 painter.end();
635 }
636 modification();
637}
638
639void BitmapImage::drawRect(QRectF rectangle, QPen pen, QBrush brush, QPainter::CompositionMode cm, bool antialiasing)
640{
641 int width = pen.width();
642 setCompositionModeBounds(rectangle.adjusted(-width, -width, width, width).toRect(), true, cm);
643 if (brush.style() == Qt::RadialGradientPattern)
644 {
645 QRadialGradient* gradient = (QRadialGradient*)brush.gradient();
646 gradient->setCenter(gradient->center() - mBounds.topLeft());
647 gradient->setFocalPoint(gradient->focalPoint() - mBounds.topLeft());
648 }
649 if (!image()->isNull())
650 {
651 QPainter painter(image());
652 painter.setCompositionMode(cm);
653 painter.setRenderHint(QPainter::Antialiasing, antialiasing);
654 painter.setPen(pen);
655 painter.setBrush(brush);
656
657 // Adjust the brush rectangle to be bigger than the bounds itself,
658 // otherwise there will be artifacts shown in some cases when smudging
659 painter.drawRect(rectangle.translated(-mBounds.topLeft()).adjusted(-1, -1, 1, 1));
660 painter.end();
661 }
662 modification();
663}
664
665void BitmapImage::drawEllipse(QRectF rectangle, QPen pen, QBrush brush, QPainter::CompositionMode cm, bool antialiasing)
666{
667 int width = pen.width();
668 setCompositionModeBounds(rectangle.adjusted(-width, -width, width, width).toRect(), true, cm);
669 if (brush.style() == Qt::RadialGradientPattern)
670 {
671 QRadialGradient* gradient = (QRadialGradient*)brush.gradient();
672 gradient->setCenter(gradient->center() - mBounds.topLeft());
673 gradient->setFocalPoint(gradient->focalPoint() - mBounds.topLeft());
674 }
675 if (!image()->isNull())
676 {
677 QPainter painter(image());
678
679 painter.setRenderHint(QPainter::Antialiasing, antialiasing);
680 painter.setPen(pen);
681 painter.setBrush(brush);
682 painter.setCompositionMode(cm);
683 painter.drawEllipse(rectangle.translated(-mBounds.topLeft()));
684 painter.end();
685 }
686 modification();
687}
688
689void BitmapImage::drawPath(QPainterPath path, QPen pen, QBrush brush,
690 QPainter::CompositionMode cm, bool antialiasing)
691{
692 int width = pen.width();
693 // qreal inc = 1.0 + width / 20.0;
694
695 setCompositionModeBounds(path.controlPointRect().adjusted(-width, -width, width, width).toRect(), true, cm);
696
697 if (!image()->isNull())
698 {
699 QPainter painter(image());
700 painter.setCompositionMode(cm);
701 painter.setRenderHint(QPainter::Antialiasing, antialiasing);
702 painter.setPen(pen);
703 painter.setBrush(brush);
704 painter.setTransform(QTransform().translate(-mBounds.left(), -mBounds.top()));
705 painter.setWorldMatrixEnabled(true);
706 if (path.length() > 0)
707 {
708 /*
709 for (int pt = 0; pt < path.elementCount() - 1; pt++)
710 {
711 qreal dx = path.elementAt(pt + 1).x - path.elementAt(pt).x;
712 qreal dy = path.elementAt(pt + 1).y - path.elementAt(pt).y;
713 qreal m = sqrt(dx*dx + dy*dy);
714 qreal factorx = dx / m;
715 qreal factory = dy / m;
716 for (float h = 0.f; h < m; h += inc)
717 {
718 qreal x = path.elementAt(pt).x + factorx * h;
719 qreal y = path.elementAt(pt).y + factory * h;
720 painter.drawPoint(QPointF(x, y));
721 }
722 }
723 */
724 painter.drawPath( path );
725 }
726 else
727 {
728 // forces drawing when points are coincident (mousedown)
729 painter.drawPoint(static_cast<int>(path.elementAt(0).x), static_cast<int>(path.elementAt(0).y));
730 }
731 painter.end();
732 }
733 modification();
734}
735
736BitmapImage* BitmapImage::scanToTransparent(BitmapImage *img, const int threshold, const bool redEnabled, const bool greenEnabled, const bool blueEnabled)
737{
738 Q_ASSERT(img != nullptr);
739
740 QRgb rgba = img->constScanLine(img->left(), img->top());
741 if (qAlpha(rgba) == 0)
742 return img;
743
744 for (int x = img->left(); x <= img->right(); x++)
745 {
746 for (int y = img->top(); y <= img->bottom(); y++)
747 {
748 rgba = img->constScanLine(x, y);
749
750 if (qAlpha(rgba) == 0)
751 break;
752
753 const int grayValue = qGray(rgba);
754 const int redValue = qRed(rgba);
755 const int greenValue = qGreen(rgba);
756 const int blueValue = qBlue(rgba);
757 if (grayValue >= threshold)
758 { // IF Threshold or above
759 img->scanLine(x, y, transp);
760 }
761 else if (redValue > greenValue + COLORDIFF &&
762 redValue > blueValue + COLORDIFF &&
763 redValue > grayValue + GRAYSCALEDIFF)
764 { // IF Red line
765 if (redEnabled)
766 {
767 img->scanLine(x, y, redline);
768 }
769 else
770 {
771 img->scanLine(x, y, transp);
772 }
773 }
774 else if (greenValue > redValue + COLORDIFF &&
775 greenValue > blueValue + COLORDIFF &&
776 greenValue > grayValue + GRAYSCALEDIFF)
777 { // IF Green line
778 if (greenEnabled)
779 {
780 img->scanLine(x, y, greenline);
781 }
782 else
783 {
784 img->scanLine(x, y, transp);
785 }
786 }
787 else if (blueValue > redValue + COLORDIFF &&
788 blueValue > greenValue + COLORDIFF &&
789 blueValue > grayValue + GRAYSCALEDIFF)
790 { // IF Blue line
791 if (blueEnabled)
792 {
793 img->scanLine(x, y, blueline);
794 }
795 else
796 {
797 img->scanLine(x, y, transp);
798 }
799 }
800 else
801 { // okay, so it is in grayscale graduation area
802 if (grayValue >= LOW_THRESHOLD)
803 {
804 const qreal factor = static_cast<qreal>(threshold - grayValue) / static_cast<qreal>(threshold - LOW_THRESHOLD);
805 img->scanLine(x , y, qRgba(0, 0, 0, static_cast<int>(threshold * factor)));
806 }
807 else // grayValue < LOW_THRESHOLD
808 {
809 img->scanLine(x , y, blackline);
810 }
811 }
812 }
813 }
814 img->modification();
815 return img;
816}
817
818Status BitmapImage::writeFile(const QString& filename)
819{
820 if (!mImage.isNull())
821 {
822 bool b = mImage.save(filename);
823 return (b) ? Status::OK : Status::FAIL;
824 }
825
826 if (bounds().isEmpty())
827 {
828 QFile f(filename);
829 if(f.exists())
830 {
831 bool b = f.remove();
832 if (!b) {
833 return Status::FAIL;
834 }
835 }
836
837 // The frame is likely empty, act like there's no file name
838 // so we don't end up writing to it later.
839 setFileName("");
840 }
841 return Status::SAFE;
842}
843
844void BitmapImage::clear()
845{
846 mImage = QImage(); // null image
847 mBounds = QRect(0, 0, 0, 0);
848 mMinBound = true;
849 modification();
850}
851
852QRgb BitmapImage::constScanLine(int x, int y) const
853{
854 QRgb result = QRgb();
855 if (mBounds.contains(x, y)) {
856 result = *(reinterpret_cast<const QRgb*>(mImage.constScanLine(y - mBounds.top())) + x - mBounds.left());
857 }
858 return result;
859}
860
861void BitmapImage::scanLine(int x, int y, QRgb color)
862{
863 if (!mBounds.contains(x, y)) {
864 return;
865 }
866 // Make sure color is premultiplied before calling
867 *(reinterpret_cast<QRgb*>(image()->scanLine(y - mBounds.top())) + x - mBounds.left()) = color;
868}
869
870void BitmapImage::clear(QRect rectangle)
871{
872 QRect clearRectangle = mBounds.intersected(rectangle);
873 clearRectangle.moveTopLeft(clearRectangle.topLeft() - mBounds.topLeft());
874
875 setCompositionModeBounds(clearRectangle, true, QPainter::CompositionMode_Clear);
876
877 QPainter painter(image());
878 painter.setCompositionMode(QPainter::CompositionMode_Clear);
879 painter.fillRect(clearRectangle, QColor(0, 0, 0, 0));
880 painter.end();
881
882 modification();
883}
884
885bool BitmapImage::floodFill(BitmapImage** replaceImage,
886 const BitmapImage* targetImage,
887 const QRect& cameraRect,
888 const QPoint& point,
889 const QRgb& fillColor,
890 int tolerance,
891 const int expandValue)
892{
893 // Fill region must be 1 pixel larger than the target image to fill regions on the edge connected only by transparent pixels
894 const QRect& fillBounds = targetImage->mBounds.adjusted(-1, -1, 1, 1);
895 QRect maxBounds = cameraRect.united(fillBounds).adjusted(-expandValue, -expandValue, expandValue, expandValue);
896 const int maxWidth = maxBounds.width(), left = maxBounds.left(), top = maxBounds.top();
897
898 // Square tolerance for use with compareColor
899 tolerance = static_cast<int>(qPow(tolerance, 2));
900
901 QRect newBounds;
902 bool *filledPixels = floodFillPoints(targetImage, maxBounds, point, tolerance, newBounds);
903
904 QRect translatedSearchBounds = newBounds.translated(-maxBounds.topLeft());
905
906 // The scanned bounds should take the expansion into account
907 const QRect& expandRect = newBounds.adjusted(-expandValue, -expandValue, expandValue, expandValue);
908 if (expandValue > 0) {
909 newBounds = expandRect;
910 }
911 if (!maxBounds.contains(newBounds)) {
912 newBounds = maxBounds;
913 }
914 translatedSearchBounds = newBounds.translated(-maxBounds.topLeft());
915
916 if (expandValue > 0) {
917 expandFill(filledPixels, translatedSearchBounds, maxBounds, expandValue);
918 }
919
920 *replaceImage = new BitmapImage(newBounds, Qt::transparent);
921
922 // Fill all the found pixels
923 for (int y = translatedSearchBounds.top(); y <= translatedSearchBounds.bottom(); y++)
924 {
925 for (int x = translatedSearchBounds.left(); x <= translatedSearchBounds.right(); x++)
926 {
927 const int index = y * maxWidth + x;
928 if (!filledPixels[index])
929 {
930 continue;
931 }
932 (*replaceImage)->scanLine(x + left, y + top, fillColor);
933 }
934 }
935
936 delete[] filledPixels;
937
938 return true;
939}
940
941// Flood filling based on this scanline algorithm
942// ----- http://lodev.org/cgtutor/floodfill.html
943bool* BitmapImage::floodFillPoints(const BitmapImage* targetImage,
944 const QRect& searchBounds,
945 QPoint point,
946 const int tolerance,
947 QRect& newBounds)
948{
949 QRgb oldColor = targetImage->constScanLine(point.x(), point.y());
950 oldColor = qRgba(qRed(oldColor), qGreen(oldColor), qBlue(oldColor), qAlpha(oldColor));
951
952 // Preparations
953 QList<QPoint> queue; // queue all the pixels of the filled area (as they are found)
954
955 QPoint tempPoint;
956 QScopedPointer< QHash<QRgb, bool> > cache(new QHash<QRgb, bool>());
957
958 int xTemp = 0;
959 bool spanLeft = false;
960 bool spanRight = false;
961
962 queue.append(point);
963 // Preparations END
964
965 bool *filledPixels = new bool[searchBounds.height()*searchBounds.width()]{};
966
967 BlitRect blitBounds(point);
968 while (!queue.empty())
969 {
970 tempPoint = queue.takeFirst();
971
972 point.setX(tempPoint.x());
973 point.setY(tempPoint.y());
974
975 xTemp = point.x();
976
977 int xCoord = xTemp - searchBounds.left();
978 int yCoord = point.y() - searchBounds.top();
979
980 if (filledPixels[yCoord*searchBounds.width()+xCoord]) continue;
981
982 while (xTemp >= searchBounds.left() &&
983 compareColor(targetImage->constScanLine(xTemp, point.y()), oldColor, tolerance, cache.data())) xTemp--;
984 xTemp++;
985
986 spanLeft = spanRight = false;
987 while (xTemp <= searchBounds.right() &&
988 compareColor(targetImage->constScanLine(xTemp, point.y()), oldColor, tolerance, cache.data()))
989 {
990
991 QPoint floodPoint = QPoint(xTemp, point.y());
992 if (!blitBounds.contains(floodPoint)) {
993 blitBounds.extend(floodPoint);
994 }
995
996 xCoord = xTemp - searchBounds.left();
997 // This pixel is what we're going to fill later
998 filledPixels[yCoord*searchBounds.width()+xCoord] = true;
999
1000 if (!spanLeft && (point.y() > searchBounds.top()) &&
1001 compareColor(targetImage->constScanLine(xTemp, point.y() - 1), oldColor, tolerance, cache.data())) {
1002 queue.append(QPoint(xTemp, point.y() - 1));
1003 spanLeft = true;
1004 }
1005 else if (spanLeft && (point.y() > searchBounds.top()) &&
1006 !compareColor(targetImage->constScanLine(xTemp, point.y() - 1), oldColor, tolerance, cache.data())) {
1007 spanLeft = false;
1008 }
1009
1010 if (!spanRight && point.y() < searchBounds.bottom() &&
1011 compareColor(targetImage->constScanLine(xTemp, point.y() + 1), oldColor, tolerance, cache.data())) {
1012 queue.append(QPoint(xTemp, point.y() + 1));
1013 spanRight = true;
1014 }
1015 else if (spanRight && point.y() < searchBounds.bottom() &&
1016 !compareColor(targetImage->constScanLine(xTemp, point.y() + 1), oldColor, tolerance, cache.data())) {
1017 spanRight = false;
1018 }
1019
1020 Q_ASSERT(queue.count() < (searchBounds.width() * searchBounds.height()));
1021 xTemp++;
1022 }
1023 }
1024
1025 newBounds = blitBounds;
1026
1027 return filledPixels;
1028}
1029
1045void BitmapImage::expandFill(bool* fillPixels, const QRect& searchBounds, const QRect& maxBounds, int expand) {
1046
1047 const int maxWidth = maxBounds.width();
1048 const int length = maxBounds.height() * maxBounds.width();
1049
1050 int* manhattanPoints = new int[length]{};
1051
1052 // Fill points with max length, this is important because otherwise the filled pixels will include a border of the expanded area
1053 std::fill_n(manhattanPoints, length, searchBounds.width()+searchBounds.height());
1054
1055 for (int y = searchBounds.top(); y <= searchBounds.bottom(); y++)
1056 {
1057 for (int x = searchBounds.left(); x <= searchBounds.right(); x++)
1058 {
1059 const int index = y*maxWidth+x;
1060
1061 if (fillPixels[index]) {
1062 manhattanPoints[index] = 0;
1063 continue;
1064 }
1065
1066 if (y > searchBounds.top()) {
1067 // the value will be the num of pixels away from y - 1 of the next position
1068 manhattanPoints[index] = qMin(manhattanPoints[index],
1069 manhattanPoints[(y - 1) * maxWidth+x] + 1);
1070
1071 int distance = manhattanPoints[index];
1072 if (distance <= expand) {
1073 fillPixels[index] = true;
1074 }
1075 }
1076 if (x > searchBounds.left()) {
1077 // the value will be the num of pixels away from x - 1 of the next position
1078 manhattanPoints[index] = qMin(manhattanPoints[index],
1079 manhattanPoints[y*maxWidth+(x - 1)] + 1);
1080
1081 int distance = manhattanPoints[index];
1082 if (distance <= expand) {
1083 fillPixels[index] = true;
1084 }
1085 }
1086 }
1087 }
1088
1089 // traverse from bottom right to top left
1090 for (int y = searchBounds.bottom(); y >= searchBounds.top(); y--)
1091 {
1092 for (int x = searchBounds.right(); x >= searchBounds.left(); x--)
1093 {
1094 const int index = y*maxWidth+x;
1095
1096 if (y + 1 < searchBounds.bottom()) {
1097 manhattanPoints[index] = qMin(manhattanPoints[index], manhattanPoints[(y + 1)*maxWidth+x] + 1);
1098
1099 int distance = manhattanPoints[index];
1100 if (distance <= expand) {
1101 fillPixels[index] = true;
1102 }
1103 }
1104 if (x + 1 < searchBounds.right()) {
1105 manhattanPoints[index] = qMin(manhattanPoints[index], manhattanPoints[y*maxWidth+(x + 1)] + 1);
1106
1107 int distance = manhattanPoints[index];
1108 if (distance <= expand) {
1109 fillPixels[index] = true;
1110 }
1111 }
1112 }
1113 }
1114
1115 delete[] manhattanPoints;
1116}
1117
BitmapImage
Definition: bitmapimage.h:28
BitmapImage::updateBounds
void updateBounds(QRect rectangle)
Update image bounds.
Definition: bitmapimage.cpp:298
BitmapImage::mMinBound
bool mMinBound
Definition: bitmapimage.h:187
BitmapImage::setCompositionModeBounds
void setCompositionModeBounds(BitmapImage *source, QPainter::CompositionMode cm)
Updates the bounds after a drawImage operation with the composition mode cm.
Definition: bitmapimage.cpp:359
BitmapImage::expandFill
static void expandFill(bool *fillPixels, const QRect &searchBounds, const QRect &maxBounds, int expand)
Finds all pixels closest to the input color and applies the input color to the image.
Definition: bitmapimage.cpp:1045
BitmapImage::autoCrop
void autoCrop()
Removes any transparent borders by reducing the boundaries.
Definition: bitmapimage.cpp:431
BitmapImage::compareColor
static bool compareColor(QRgb newColor, QRgb oldColor, int tolerance, QHash< QRgb, bool > *cache)
Compare colors for the purposes of flood filling.
Definition: bitmapimage.h:146
BlitRect
Definition: blitrect.h:25
KeyFrame
Definition: keyframe.h:30
Status
Definition: pencilerror.h:40
Tile
Definition: tile.h:24
TiledBuffer
Definition: tiledbuffer.h:49
QBrush
QBrush::gradient
const QGradient * gradient() const const
QBrush::style
Qt::BrushStyle style() const const
QColor
QColor::rgba
QRgb rgba() const const
QFile
QFile::copy
bool copy(const QString &newName)
QFile::exists
bool exists() const const
QFileInfo
QHash
QImage::transformed
QImage transformed(const QMatrix &matrix, Qt::TransformationMode mode) const const
QImage
QImage::Format_ARGB32_Premultiplied
Format_ARGB32_Premultiplied
QImage::constScanLine
const uchar * constScanLine(int i) const const
QImage::convertToFormat
QImage convertToFormat(QImage::Format format, Qt::ImageConversionFlags flags) const &const
QImage::fill
void fill(uint pixelValue)
QImage::format
QImage::Format format() const const
QImage::isNull
bool isNull() const const
QImage::pixel
QRgb pixel(int x, int y) const const
QImage::save
bool save(const QString &fileName, const char *format, int quality) const const
QImage::scanLine
uchar * scanLine(int i)
QImage::setPixel
void setPixel(int x, int y, uint index_or_rgb)
QImage::size
QSize size() const const
QImage::width
int width() const const
QList
QList::append
void append(const T &value)
QList::count
int count(const T &value) const const
QList::empty
bool empty() const const
QList::takeFirst
T takeFirst()
QPainter
QPainter::CompositionMode
CompositionMode
QPainter::SmoothPixmapTransform
SmoothPixmapTransform
QPainter::drawEllipse
void drawEllipse(const QRectF &rectangle)
QPainter::drawImage
void drawImage(const QRectF &target, const QImage &image, const QRectF &source, Qt::ImageConversionFlags flags)
QPainter::drawLine
void drawLine(const QLineF &line)
QPainter::drawPath
void drawPath(const QPainterPath &path)
QPainter::drawPixmap
void drawPixmap(const QRectF &target, const QPixmap &pixmap, const QRectF &source)
QPainter::drawPoint
void drawPoint(const QPointF &position)
QPainter::drawRect
void drawRect(const QRectF &rectangle)
QPainter::end
bool end()
QPainter::fillRect
void fillRect(const QRectF &rectangle, const QBrush &brush)
QPainter::setBrush
void setBrush(const QBrush &brush)
QPainter::setCompositionMode
void setCompositionMode(QPainter::CompositionMode mode)
QPainter::setPen
void setPen(const QColor &color)
QPainter::setRenderHint
void setRenderHint(QPainter::RenderHint hint, bool on)
QPainter::setTransform
void setTransform(const QTransform &transform, bool combine)
QPainter::setWorldMatrixEnabled
void setWorldMatrixEnabled(bool enable)
QPainterPath
QPainterPath::controlPointRect
QRectF controlPointRect() const const
QPainterPath::elementAt
QPainterPath::Element elementAt(int index) const const
QPainterPath::length
qreal length() const const
QPen
QPen::width
int width() const const
QPixmap
QPoint
QPoint::setX
void setX(int x)
QPoint::setY
void setY(int y)
QPoint::x
int x() const const
QPoint::y
int y() const const
QPointF
QPointF::toPoint
QPoint toPoint() const const
QRadialGradient
QRadialGradient::center
QPointF center() const const
QRadialGradient::focalPoint
QPointF focalPoint() const const
QRadialGradient::setCenter
void setCenter(const QPointF &center)
QRadialGradient::setFocalPoint
void setFocalPoint(const QPointF &focalPoint)
QRect
QRect::adjusted
QRect adjusted(int dx1, int dy1, int dx2, int dy2) const const
QRect::bottom
int bottom() const const
QRect::contains
bool contains(const QRect &rectangle, bool proper) const const
QRect::height
int height() const const
QRect::intersected
QRect intersected(const QRect &rectangle) const const
QRect::isEmpty
bool isEmpty() const const
QRect::left
int left() const const
QRect::moveTopLeft
void moveTopLeft(const QPoint &position)
QRect::normalized
QRect normalized() const const
QRect::right
int right() const const
QRect::setHeight
void setHeight(int height)
QRect::setSize
void setSize(const QSize &size)
QRect::setWidth
void setWidth(int width)
QRect::size
QSize size() const const
QRect::top
int top() const const
QRect::topLeft
QPoint topLeft() const const
QRect::translated
QRect translated(int dx, int dy) const const
QRect::united
QRect united(const QRect &rectangle) const const
QRect::width
int width() const const
QRectF
QRectF::adjusted
QRectF adjusted(qreal dx1, qreal dy1, qreal dx2, qreal dy2) const const
QRectF::toRect
QRect toRect() const const
QRectF::translated
QRectF translated(qreal dx, qreal dy) const const
QScopedPointer
QSize
QString
QString::arg
QString arg(qlonglong a, int fieldWidth, int base, QChar fillChar) const const
QString::isEmpty
bool isEmpty() const const
Qt::RadialGradientPattern
RadialGradientPattern
Qt::transparent
transparent
Qt::SmoothTransformation
SmoothTransformation
QTransform
QTransform::mapRect
QRect mapRect(const QRect &rectangle) const const
Generated on Thu May 8 2025 04:47:53 for Pencil2D by doxygen 1.9.6 based on revision 4513250b1d5b1a3676ec0e67b06b7a885ceaae39