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 if (relTop > relBottom || relLeft > relRight) {
578 clear();
579 return;
580 }
581 //qDebug() << "Original" << mBounds;
582 //qDebug() << "Autocrop" << relLeft << relTop << relRight - mBounds.width() + 1 << relBottom - mBounds.height() + 1;
583 // Update mBounds and mImage if necessary
584 updateBounds(mBounds.adjusted(relLeft, relTop, relRight - mBounds.width() + 1, relBottom - mBounds.height() + 1));
585
586 //qDebug() << "New bounds" << mBounds;
587
588 mMinBound = true;
589}
590
591QRgb BitmapImage::pixel(int x, int y)
592{
593 return pixel(QPoint(x, y));
594}
595
596QRgb BitmapImage::pixel(QPoint p)
597{
598 QRgb result = qRgba(0, 0, 0, 0); // black
599 if (mBounds.contains(p))
600 result = image()->pixel(p - mBounds.topLeft());
601 return result;
602}
603
604void BitmapImage::setPixel(int x, int y, QRgb color)
605{
606 setPixel(QPoint(x, y), color);
607}
608
609void BitmapImage::setPixel(QPoint p, QRgb color)
610{
611 setCompositionModeBounds(QRect(p, QSize(1,1)), true, QPainter::CompositionMode_SourceOver);
612 if (mBounds.contains(p))
613 {
614 image()->setPixel(p - mBounds.topLeft(), color);
615 }
616 modification();
617}
618
619void BitmapImage::fillNonAlphaPixels(const QRgb color)
620{
621 if (mBounds.isEmpty()) { return; }
622
623 BitmapImage fill(bounds(), color);
624 paste(&fill, QPainter::CompositionMode_SourceIn);
625}
626
627void BitmapImage::drawLine(QPointF P1, QPointF P2, QPen pen, QPainter::CompositionMode cm, bool antialiasing)
628{
629 int width = 2 + pen.width();
630 setCompositionModeBounds(QRect(P1.toPoint(), P2.toPoint()).normalized().adjusted(-width, -width, width, width), true, cm);
631 if (!image()->isNull())
632 {
633 QPainter painter(image());
634 painter.setCompositionMode(cm);
635 painter.setRenderHint(QPainter::Antialiasing, antialiasing);
636 painter.setPen(pen);
637 painter.drawLine(P1 - mBounds.topLeft(), P2 - mBounds.topLeft());
638 painter.end();
639 }
640 modification();
641}
642
643void BitmapImage::drawRect(QRectF rectangle, QPen pen, QBrush brush, QPainter::CompositionMode cm, bool antialiasing)
644{
645 int width = pen.width();
646 setCompositionModeBounds(rectangle.adjusted(-width, -width, width, width).toRect(), true, cm);
647 if (brush.style() == Qt::RadialGradientPattern)
648 {
649 QRadialGradient* gradient = (QRadialGradient*)brush.gradient();
650 gradient->setCenter(gradient->center() - mBounds.topLeft());
651 gradient->setFocalPoint(gradient->focalPoint() - mBounds.topLeft());
652 }
653 if (!image()->isNull())
654 {
655 QPainter painter(image());
656 painter.setCompositionMode(cm);
657 painter.setRenderHint(QPainter::Antialiasing, antialiasing);
658 painter.setPen(pen);
659 painter.setBrush(brush);
660
661 // Adjust the brush rectangle to be bigger than the bounds itself,
662 // otherwise there will be artifacts shown in some cases when smudging
663 painter.drawRect(rectangle.translated(-mBounds.topLeft()).adjusted(-1, -1, 1, 1));
664 painter.end();
665 }
666 modification();
667}
668
669void BitmapImage::drawEllipse(QRectF rectangle, QPen pen, QBrush brush, QPainter::CompositionMode cm, bool antialiasing)
670{
671 int width = pen.width();
672 setCompositionModeBounds(rectangle.adjusted(-width, -width, width, width).toRect(), true, cm);
673 if (brush.style() == Qt::RadialGradientPattern)
674 {
675 QRadialGradient* gradient = (QRadialGradient*)brush.gradient();
676 gradient->setCenter(gradient->center() - mBounds.topLeft());
677 gradient->setFocalPoint(gradient->focalPoint() - mBounds.topLeft());
678 }
679 if (!image()->isNull())
680 {
681 QPainter painter(image());
682
683 painter.setRenderHint(QPainter::Antialiasing, antialiasing);
684 painter.setPen(pen);
685 painter.setBrush(brush);
686 painter.setCompositionMode(cm);
687 painter.drawEllipse(rectangle.translated(-mBounds.topLeft()));
688 painter.end();
689 }
690 modification();
691}
692
693void BitmapImage::drawPath(QPainterPath path, QPen pen, QBrush brush,
694 QPainter::CompositionMode cm, bool antialiasing)
695{
696 int width = pen.width();
697 // qreal inc = 1.0 + width / 20.0;
698
699 setCompositionModeBounds(path.controlPointRect().adjusted(-width, -width, width, width).toRect(), true, cm);
700
701 if (!image()->isNull())
702 {
703 QPainter painter(image());
704 painter.setCompositionMode(cm);
705 painter.setRenderHint(QPainter::Antialiasing, antialiasing);
706 painter.setPen(pen);
707 painter.setBrush(brush);
708 painter.setTransform(QTransform().translate(-mBounds.left(), -mBounds.top()));
709 painter.setWorldMatrixEnabled(true);
710 if (path.length() > 0)
711 {
712 /*
713 for (int pt = 0; pt < path.elementCount() - 1; pt++)
714 {
715 qreal dx = path.elementAt(pt + 1).x - path.elementAt(pt).x;
716 qreal dy = path.elementAt(pt + 1).y - path.elementAt(pt).y;
717 qreal m = sqrt(dx*dx + dy*dy);
718 qreal factorx = dx / m;
719 qreal factory = dy / m;
720 for (float h = 0.f; h < m; h += inc)
721 {
722 qreal x = path.elementAt(pt).x + factorx * h;
723 qreal y = path.elementAt(pt).y + factory * h;
724 painter.drawPoint(QPointF(x, y));
725 }
726 }
727 */
728 painter.drawPath( path );
729 }
730 else
731 {
732 // forces drawing when points are coincident (mousedown)
733 painter.drawPoint(static_cast<int>(path.elementAt(0).x), static_cast<int>(path.elementAt(0).y));
734 }
735 painter.end();
736 }
737 modification();
738}
739
740BitmapImage* BitmapImage::scanToTransparent(BitmapImage *img, const int threshold, const bool redEnabled, const bool greenEnabled, const bool blueEnabled)
741{
742 Q_ASSERT(img != nullptr);
743
744 QRgb rgba = img->constScanLine(img->left(), img->top());
745 if (qAlpha(rgba) == 0)
746 return img;
747
748 for (int x = img->left(); x <= img->right(); x++)
749 {
750 for (int y = img->top(); y <= img->bottom(); y++)
751 {
752 rgba = img->constScanLine(x, y);
753
754 if (qAlpha(rgba) == 0)
755 break;
756
757 const int grayValue = qGray(rgba);
758 const int redValue = qRed(rgba);
759 const int greenValue = qGreen(rgba);
760 const int blueValue = qBlue(rgba);
761 if (grayValue >= threshold)
762 { // IF Threshold or above
763 img->scanLine(x, y, transp);
764 }
765 else if (redValue > greenValue + COLORDIFF &&
766 redValue > blueValue + COLORDIFF &&
767 redValue > grayValue + GRAYSCALEDIFF)
768 { // IF Red line
769 if (redEnabled)
770 {
771 img->scanLine(x, y, redline);
772 }
773 else
774 {
775 img->scanLine(x, y, transp);
776 }
777 }
778 else if (greenValue > redValue + COLORDIFF &&
779 greenValue > blueValue + COLORDIFF &&
780 greenValue > grayValue + GRAYSCALEDIFF)
781 { // IF Green line
782 if (greenEnabled)
783 {
784 img->scanLine(x, y, greenline);
785 }
786 else
787 {
788 img->scanLine(x, y, transp);
789 }
790 }
791 else if (blueValue > redValue + COLORDIFF &&
792 blueValue > greenValue + COLORDIFF &&
793 blueValue > grayValue + GRAYSCALEDIFF)
794 { // IF Blue line
795 if (blueEnabled)
796 {
797 img->scanLine(x, y, blueline);
798 }
799 else
800 {
801 img->scanLine(x, y, transp);
802 }
803 }
804 else
805 { // okay, so it is in grayscale graduation area
806 if (grayValue >= LOW_THRESHOLD)
807 {
808 const qreal factor = static_cast<qreal>(threshold - grayValue) / static_cast<qreal>(threshold - LOW_THRESHOLD);
809 img->scanLine(x , y, qRgba(0, 0, 0, static_cast<int>(threshold * factor)));
810 }
811 else // grayValue < LOW_THRESHOLD
812 {
813 img->scanLine(x , y, blackline);
814 }
815 }
816 }
817 }
818 img->modification();
819 return img;
820}
821
822Status BitmapImage::writeFile(const QString& filename)
823{
824 if (!mImage.isNull())
825 {
826 bool b = mImage.save(filename);
827 return (b) ? Status::OK : Status::FAIL;
828 }
829
830 if (bounds().isEmpty())
831 {
832 QFile f(filename);
833 if(f.exists())
834 {
835 bool b = f.remove();
836 if (!b) {
837 return Status::FAIL;
838 }
839 }
840
841 // The frame is likely empty, act like there's no file name
842 // so we don't end up writing to it later.
843 setFileName("");
844 }
845 return Status::SAFE;
846}
847
848void BitmapImage::clear()
849{
850 mImage = QImage(); // null image
851 mBounds = QRect(0, 0, 0, 0);
852 mMinBound = true;
853 modification();
854}
855
856QRgb BitmapImage::constScanLine(int x, int y) const
857{
858 QRgb result = QRgb();
859 if (mBounds.contains(x, y)) {
860 result = *(reinterpret_cast<const QRgb*>(mImage.constScanLine(y - mBounds.top())) + x - mBounds.left());
861 }
862 return result;
863}
864
865void BitmapImage::scanLine(int x, int y, QRgb color)
866{
867 if (!mBounds.contains(x, y)) {
868 return;
869 }
870 // Make sure color is premultiplied before calling
871 *(reinterpret_cast<QRgb*>(image()->scanLine(y - mBounds.top())) + x - mBounds.left()) = color;
872}
873
874void BitmapImage::clear(QRect rectangle)
875{
876 QRect clearRectangle = mBounds.intersected(rectangle);
877 clearRectangle.moveTopLeft(clearRectangle.topLeft() - mBounds.topLeft());
878
879 setCompositionModeBounds(clearRectangle, true, QPainter::CompositionMode_Clear);
880
881 QPainter painter(image());
882 painter.setCompositionMode(QPainter::CompositionMode_Clear);
883 painter.fillRect(clearRectangle, QColor(0, 0, 0, 0));
884 painter.end();
885
886 modification();
887}
888
889bool BitmapImage::floodFill(BitmapImage** replaceImage,
890 const BitmapImage* targetImage,
891 const QRect& cameraRect,
892 const QPoint& point,
893 const QRgb& fillColor,
894 int tolerance,
895 const int expandValue)
896{
897 // Fill region must be 1 pixel larger than the target image to fill regions on the edge connected only by transparent pixels
898 const QRect& fillBounds = targetImage->mBounds.adjusted(-1, -1, 1, 1);
899 QRect maxBounds = cameraRect.united(fillBounds).adjusted(-expandValue, -expandValue, expandValue, expandValue);
900 const int maxWidth = maxBounds.width(), left = maxBounds.left(), top = maxBounds.top();
901
902 // Square tolerance for use with compareColor
903 tolerance = static_cast<int>(qPow(tolerance, 2));
904
905 QRect newBounds;
906 bool *filledPixels = floodFillPoints(targetImage, maxBounds, point, tolerance, newBounds);
907
908 QRect translatedSearchBounds = newBounds.translated(-maxBounds.topLeft());
909
910 // The scanned bounds should take the expansion into account
911 const QRect& expandRect = newBounds.adjusted(-expandValue, -expandValue, expandValue, expandValue);
912 if (expandValue > 0) {
913 newBounds = expandRect;
914 }
915 if (!maxBounds.contains(newBounds)) {
916 newBounds = maxBounds;
917 }
918 translatedSearchBounds = newBounds.translated(-maxBounds.topLeft());
919
920 if (expandValue > 0) {
921 expandFill(filledPixels, translatedSearchBounds, maxBounds, expandValue);
922 }
923
924 *replaceImage = new BitmapImage(newBounds, Qt::transparent);
925
926 // Fill all the found pixels
927 for (int y = translatedSearchBounds.top(); y <= translatedSearchBounds.bottom(); y++)
928 {
929 for (int x = translatedSearchBounds.left(); x <= translatedSearchBounds.right(); x++)
930 {
931 const int index = y * maxWidth + x;
932 if (!filledPixels[index])
933 {
934 continue;
935 }
936 (*replaceImage)->scanLine(x + left, y + top, fillColor);
937 }
938 }
939
940 delete[] filledPixels;
941
942 return true;
943}
944
945// Flood filling based on this scanline algorithm
946// ----- http://lodev.org/cgtutor/floodfill.html
947bool* BitmapImage::floodFillPoints(const BitmapImage* targetImage,
948 const QRect& searchBounds,
949 QPoint point,
950 const int tolerance,
951 QRect& newBounds)
952{
953 QRgb oldColor = targetImage->constScanLine(point.x(), point.y());
954 oldColor = qRgba(qRed(oldColor), qGreen(oldColor), qBlue(oldColor), qAlpha(oldColor));
955
956 // Preparations
957 QList<QPoint> queue; // queue all the pixels of the filled area (as they are found)
958
959 QPoint tempPoint;
960 QScopedPointer< QHash<QRgb, bool> > cache(new QHash<QRgb, bool>());
961
962 int xTemp = 0;
963 bool spanLeft = false;
964 bool spanRight = false;
965
966 queue.append(point);
967 // Preparations END
968
969 bool *filledPixels = new bool[searchBounds.height()*searchBounds.width()]{};
970
971 BlitRect blitBounds(point);
972 while (!queue.empty())
973 {
974 tempPoint = queue.takeFirst();
975
976 point.setX(tempPoint.x());
977 point.setY(tempPoint.y());
978
979 xTemp = point.x();
980
981 int xCoord = xTemp - searchBounds.left();
982 int yCoord = point.y() - searchBounds.top();
983
984 if (filledPixels[yCoord*searchBounds.width()+xCoord]) continue;
985
986 while (xTemp >= searchBounds.left() &&
987 compareColor(targetImage->constScanLine(xTemp, point.y()), oldColor, tolerance, cache.data())) xTemp--;
988 xTemp++;
989
990 spanLeft = spanRight = false;
991 while (xTemp <= searchBounds.right() &&
992 compareColor(targetImage->constScanLine(xTemp, point.y()), oldColor, tolerance, cache.data()))
993 {
994
995 QPoint floodPoint = QPoint(xTemp, point.y());
996 if (!blitBounds.contains(floodPoint)) {
997 blitBounds.extend(floodPoint);
998 }
999
1000 xCoord = xTemp - searchBounds.left();
1001 // This pixel is what we're going to fill later
1002 filledPixels[yCoord*searchBounds.width()+xCoord] = true;
1003
1004 if (!spanLeft && (point.y() > searchBounds.top()) &&
1005 compareColor(targetImage->constScanLine(xTemp, point.y() - 1), oldColor, tolerance, cache.data())) {
1006 queue.append(QPoint(xTemp, point.y() - 1));
1007 spanLeft = true;
1008 }
1009 else if (spanLeft && (point.y() > searchBounds.top()) &&
1010 !compareColor(targetImage->constScanLine(xTemp, point.y() - 1), oldColor, tolerance, cache.data())) {
1011 spanLeft = false;
1012 }
1013
1014 if (!spanRight && point.y() < searchBounds.bottom() &&
1015 compareColor(targetImage->constScanLine(xTemp, point.y() + 1), oldColor, tolerance, cache.data())) {
1016 queue.append(QPoint(xTemp, point.y() + 1));
1017 spanRight = true;
1018 }
1019 else if (spanRight && point.y() < searchBounds.bottom() &&
1020 !compareColor(targetImage->constScanLine(xTemp, point.y() + 1), oldColor, tolerance, cache.data())) {
1021 spanRight = false;
1022 }
1023
1024 Q_ASSERT(queue.count() < (searchBounds.width() * searchBounds.height()));
1025 xTemp++;
1026 }
1027 }
1028
1029 newBounds = blitBounds;
1030
1031 return filledPixels;
1032}
1033
1049void BitmapImage::expandFill(bool* fillPixels, const QRect& searchBounds, const QRect& maxBounds, int expand) {
1050
1051 const int maxWidth = maxBounds.width();
1052 const int length = maxBounds.height() * maxBounds.width();
1053
1054 int* manhattanPoints = new int[length]{};
1055
1056 // Fill points with max length, this is important because otherwise the filled pixels will include a border of the expanded area
1057 std::fill_n(manhattanPoints, length, searchBounds.width()+searchBounds.height());
1058
1059 for (int y = searchBounds.top(); y <= searchBounds.bottom(); y++)
1060 {
1061 for (int x = searchBounds.left(); x <= searchBounds.right(); x++)
1062 {
1063 const int index = y*maxWidth+x;
1064
1065 if (fillPixels[index]) {
1066 manhattanPoints[index] = 0;
1067 continue;
1068 }
1069
1070 if (y > searchBounds.top()) {
1071 // the value will be the num of pixels away from y - 1 of the next position
1072 manhattanPoints[index] = qMin(manhattanPoints[index],
1073 manhattanPoints[(y - 1) * maxWidth+x] + 1);
1074
1075 int distance = manhattanPoints[index];
1076 if (distance <= expand) {
1077 fillPixels[index] = true;
1078 }
1079 }
1080 if (x > searchBounds.left()) {
1081 // the value will be the num of pixels away from x - 1 of the next position
1082 manhattanPoints[index] = qMin(manhattanPoints[index],
1083 manhattanPoints[y*maxWidth+(x - 1)] + 1);
1084
1085 int distance = manhattanPoints[index];
1086 if (distance <= expand) {
1087 fillPixels[index] = true;
1088 }
1089 }
1090 }
1091 }
1092
1093 // traverse from bottom right to top left
1094 for (int y = searchBounds.bottom(); y >= searchBounds.top(); y--)
1095 {
1096 for (int x = searchBounds.right(); x >= searchBounds.left(); x--)
1097 {
1098 const int index = y*maxWidth+x;
1099
1100 if (y + 1 < searchBounds.bottom()) {
1101 manhattanPoints[index] = qMin(manhattanPoints[index], manhattanPoints[(y + 1)*maxWidth+x] + 1);
1102
1103 int distance = manhattanPoints[index];
1104 if (distance <= expand) {
1105 fillPixels[index] = true;
1106 }
1107 }
1108 if (x + 1 < searchBounds.right()) {
1109 manhattanPoints[index] = qMin(manhattanPoints[index], manhattanPoints[y*maxWidth+(x + 1)] + 1);
1110
1111 int distance = manhattanPoints[index];
1112 if (distance <= expand) {
1113 fillPixels[index] = true;
1114 }
1115 }
1116 }
1117 }
1118
1119 delete[] manhattanPoints;
1120}
1121
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:1049
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
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 Tue Nov 18 2025 07:45:09 for Pencil2D by doxygen 1.9.6 based on revision d32017e1e1c82fd8b2e48782fe8069d83af428e2