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 <QDebug>
20#include <QDir>
21#include <QFile>
22#include <QFileInfo>
23#include <QImageWriter>
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 && !fileName().isEmpty())
143 {
144 mImage = QImage();
145 }
146}
147
148bool BitmapImage::isLoaded() const
149{
150 if (mImage.isNull()) { return false; }
151
152 return mImage.width() == mBounds.width();
153}
154
155quint64 BitmapImage::memoryUsage()
156{
157 if (!mImage.isNull())
158 {
159 return imageSize(mImage);
160 }
161 return 0;
162}
163
164void BitmapImage::paintImage(QPainter& painter)
165{
166 painter.drawImage(mBounds.topLeft(), *image());
167}
168
169void BitmapImage::paintImage(QPainter& painter, QImage& image, QRect sourceRect, QRect destRect)
170{
171 painter.drawImage(QRect(mBounds.topLeft(), destRect.size()),
172 image,
173 sourceRect);
174}
175
176QImage* BitmapImage::image()
177{
178 loadFile();
179 return &mImage;
180}
181
182BitmapImage BitmapImage::copy()
183{
184 return BitmapImage(mBounds.topLeft(), *image());
185}
186
187BitmapImage BitmapImage::copy(QRect rectangle)
188{
189 if (rectangle.isEmpty() || mBounds.isEmpty()) return BitmapImage();
190
191 QRect intersection2 = rectangle.translated(-mBounds.topLeft());
192
193 BitmapImage result(rectangle.topLeft(), image()->copy(intersection2));
194 return result;
195}
196
197void BitmapImage::paste(BitmapImage* bitmapImage, QPainter::CompositionMode cm)
198{
199 if(bitmapImage->width() <= 0 || bitmapImage->height() <= 0)
200 {
201 return;
202 }
203
204 setCompositionModeBounds(bitmapImage, cm);
205
206 QImage* image2 = bitmapImage->image();
207
208 QPainter painter(image());
209 painter.setCompositionMode(cm);
210 painter.drawImage(bitmapImage->mBounds.topLeft() - mBounds.topLeft(), *image2);
211 painter.end();
212
213 modification();
214}
215
216void BitmapImage::paste(const TiledBuffer* tiledBuffer, QPainter::CompositionMode cm)
217{
218 if(tiledBuffer->bounds().width() <= 0 || tiledBuffer->bounds().height() <= 0)
219 {
220 return;
221 }
222 extend(tiledBuffer->bounds());
223
224 QPainter painter(image());
225
226 painter.setCompositionMode(cm);
227 auto const tiles = tiledBuffer->tiles();
228 for (const Tile* item : tiles) {
229 const QPixmap& tilePixmap = item->pixmap();
230 const QPoint& tilePos = item->pos();
231 painter.drawPixmap(tilePos-mBounds.topLeft(), tilePixmap);
232 }
233 painter.end();
234
235 modification();
236}
237
238void BitmapImage::moveTopLeft(QPoint point)
239{
240 mBounds.moveTopLeft(point);
241 // Size is unchanged so there is no need to update mBounds
242 modification();
243}
244
245void BitmapImage::transform(QRect newBoundaries, bool smoothTransform)
246{
247 mBounds = newBoundaries;
248 newBoundaries.moveTopLeft(QPoint(0, 0));
249 QImage newImage(mBounds.size(), QImage::Format_ARGB32_Premultiplied);
250
251 QPainter painter(&newImage);
252 painter.setRenderHint(QPainter::SmoothPixmapTransform, smoothTransform);
253 painter.setCompositionMode(QPainter::CompositionMode_Source);
254 painter.fillRect(newImage.rect(), QColor(0, 0, 0, 0));
255 painter.setCompositionMode(QPainter::CompositionMode_SourceOver);
256 painter.drawImage(newBoundaries, *image());
257 painter.end();
258 mImage = newImage;
259
260 modification();
261}
262
263BitmapImage BitmapImage::transformed(QRect selection, QTransform transform, bool smoothTransform)
264{
265 Q_ASSERT(!selection.isEmpty());
266
267 BitmapImage selectedPart = copy(selection);
268
269 // Get the transformed image
270 QImage transformedImage;
271 if (smoothTransform)
272 {
273 transformedImage = selectedPart.image()->transformed(transform, Qt::SmoothTransformation);
274 }
275 else
276 {
277 transformedImage = selectedPart.image()->transformed(transform);
278 }
279 return BitmapImage(transform.mapRect(selection).normalized().topLeft(), transformedImage);
280}
281
282BitmapImage BitmapImage::transformed(QRect newBoundaries, bool smoothTransform)
283{
284 BitmapImage transformedImage(newBoundaries, QColor(0, 0, 0, 0));
285 QPainter painter(transformedImage.image());
286 painter.setRenderHint(QPainter::SmoothPixmapTransform, smoothTransform);
287 newBoundaries.moveTopLeft(QPoint(0, 0));
288 painter.drawImage(newBoundaries, *image());
289 painter.end();
290 return transformedImage;
291}
292
300void BitmapImage::updateBounds(QRect newBoundaries)
301{
302 // Check to make sure changes actually need to be made
303 if (mBounds == newBoundaries) return;
304
305 QImage newImage(newBoundaries.size(), QImage::Format_ARGB32_Premultiplied);
306 newImage.fill(Qt::transparent);
307 if (!newImage.isNull())
308 {
309 QPainter painter(&newImage);
310 painter.drawImage(mBounds.topLeft() - newBoundaries.topLeft(), mImage);
311 painter.end();
312 }
313 mImage = newImage;
314 mBounds = newBoundaries;
315 mMinBound = false;
316
317 modification();
318}
319
320void BitmapImage::extend(const QPoint &p)
321{
322 if (!mBounds.contains(p))
323 {
324 extend(QRect(p, QSize(1, 1)));
325 }
326}
327
328void BitmapImage::extend(QRect rectangle)
329{
330 if (rectangle.width() <= 0) rectangle.setWidth(1);
331 if (rectangle.height() <= 0) rectangle.setHeight(1);
332 if (mBounds.contains(rectangle))
333 {
334 // Do nothing
335 }
336 else
337 {
338 QRect newBoundaries = mBounds.united(rectangle).normalized();
339 QImage newImage(newBoundaries.size(), QImage::Format_ARGB32_Premultiplied);
340 newImage.fill(Qt::transparent);
341 if (!newImage.isNull())
342 {
343 QPainter painter(&newImage);
344 painter.drawImage(mBounds.topLeft() - newBoundaries.topLeft(), *image());
345 painter.end();
346 }
347 mImage = newImage;
348 mBounds = newBoundaries;
349
350 modification();
351 }
352}
353
361void BitmapImage::setCompositionModeBounds(BitmapImage *source, QPainter::CompositionMode cm)
362{
363 if (source)
364 {
365 setCompositionModeBounds(source->mBounds, source->mMinBound, cm);
366 }
367}
368
388void BitmapImage::setCompositionModeBounds(QRect sourceBounds, bool isSourceMinBounds, QPainter::CompositionMode cm)
389{
390 QRect newBoundaries;
391 switch(cm)
392 {
393 case QPainter::CompositionMode_Destination:
394 case QPainter::CompositionMode_SourceAtop:
395 // The Destination and SourceAtop modes
396 // do not change the bounds from destination.
397 newBoundaries = mBounds;
398 // mMinBound remains the same
399 break;
400 case QPainter::CompositionMode_SourceIn:
401 case QPainter::CompositionMode_DestinationIn:
402 case QPainter::CompositionMode_Clear:
403 case QPainter::CompositionMode_DestinationOut:
404 // The bounds of the result of SourceIn, DestinationIn, Clear, and DestinationOut
405 // modes are no larger than the destination bounds
406 newBoundaries = mBounds;
407 mMinBound = false;
408 break;
409 default:
410 // If it's not one of the above cases, create a union of the two bounds.
411 // This contains the minimum bounds, if both the destination and source
412 // use their respective minimum bounds.
413 newBoundaries = mBounds.united(sourceBounds);
414 mMinBound = mMinBound && isSourceMinBounds;
415 }
416
417 updateBounds(newBoundaries);
418}
419
433void BitmapImage::autoCrop()
434{
435 if (!mEnableAutoCrop) return;
436 if (mBounds.isEmpty()) return; // Exit if current bounds are null
437 if (mImage.isNull()) return;
438
439 Q_ASSERT(mBounds.size() == mImage.size());
440
441 // Exit if already min bounded
442 if (mMinBound) return;
443
444 // Get image properties
445 const int width = mImage.width();
446
447 // Relative top and bottom row indices (inclusive)
448 int relTop = 0;
449 int relBottom = mBounds.height() - 1;
450
451 // Check top row
452 bool isEmpty = true; // Used to track if a non-transparent pixel has been found
453 while (isEmpty && relTop <= relBottom) // Loop through rows
454 {
455 // Point cursor to the first pixel in the current top row
456 const QRgb* cursor = reinterpret_cast<const QRgb*>(mImage.constScanLine(relTop));
457 for (int col = 0; col < width; col++) // Loop through pixels in row
458 {
459 // If the pixel is not transparent
460 // (i.e. alpha channel > 0)
461 if (qAlpha(*cursor) != 0)
462 {
463 // We've found a non-transparent pixel in row relTop,
464 // so we can stop looking for one
465 isEmpty = false;
466 break;
467 }
468 // Move cursor to point to the next pixel in the row
469 cursor++;
470 }
471 if (isEmpty)
472 {
473 // If the row we just checked was empty, increase relTop
474 // to remove the empty row from the top of the bounding box
475 ++relTop;
476 }
477 }
478
479 // Check bottom row
480 isEmpty = true; // Reset isEmpty
481 while (isEmpty && relBottom >= relTop) // Loop through rows
482 {
483 // Point cursor to the first pixel in the current bottom row
484 const QRgb* cursor = reinterpret_cast<const QRgb*>(mImage.constScanLine(relBottom));
485 for (int col = 0; col < width; col++) // Loop through pixels in row
486 {
487 // If the pixel is not transparent
488 // (i.e. alpha channel > 0)
489 if(qAlpha(*cursor) != 0)
490 {
491 // We've found a non-transparent pixel in row relBottom,
492 // so we can stop looking for one
493 isEmpty = false;
494 break;
495 }
496 // Move cursor to point to the next pixel in the row
497 ++cursor;
498 }
499 if (isEmpty)
500 {
501 // If the row we just checked was empty, decrease relBottom
502 // to remove the empty row from the bottom of the bounding box
503 --relBottom;
504 }
505 }
506
507 // Relative left and right column indices (inclusive)
508 int relLeft = 0;
509 int relRight = mBounds.width()-1;
510
511 // Check left column - find minimum transparent span at start of each row
512 int minLeft = mBounds.width();
513 for (int row = relTop; row <= relBottom; ++row)
514 {
515 const QRgb* cursor = reinterpret_cast<const QRgb*>(mImage.constScanLine(row));
516 for (int col = 0; col < minLeft; ++col)
517 {
518 if (qAlpha(*cursor) != 0)
519 {
520 minLeft = col;
521 break;
522 }
523 ++cursor;
524 }
525 }
526 relLeft = minLeft;
527
528 // Check right column - find minimum transparent span at end of each row
529 int minRight = 0;
530 for (int row = relTop; row <= relBottom; ++row)
531 {
532 const QRgb* cursor = reinterpret_cast<const QRgb*>(mImage.constScanLine(row)) + mBounds.width() - 1;
533 for (int col = mBounds.width() - 1; col > minRight; --col)
534 {
535 if (qAlpha(*cursor) != 0)
536 {
537 minRight = col;
538 break;
539 }
540 --cursor;
541 }
542 }
543 relRight = minRight;
544
545 if (relTop > relBottom || relLeft > relRight)
546 {
547 clear();
548 return;
549 }
550 //qDebug() << "Original" << mBounds;
551 //qDebug() << "Autocrop" << relLeft << relTop << relRight - mBounds.width() + 1 << relBottom - mBounds.height() + 1;
552 // Update mBounds and mImage if necessary
553 updateBounds(mBounds.adjusted(relLeft, relTop, relRight - mBounds.width() + 1, relBottom - mBounds.height() + 1));
554
555 //qDebug() << "New bounds" << mBounds;
556
557 mMinBound = true;
558}
559
560QRgb BitmapImage::pixel(int x, int y)
561{
562 return pixel(QPoint(x, y));
563}
564
565QRgb BitmapImage::pixel(QPoint p)
566{
567 QRgb result = qRgba(0, 0, 0, 0); // black
568 if (mBounds.contains(p))
569 result = image()->pixel(p - mBounds.topLeft());
570 return result;
571}
572
573void BitmapImage::setPixel(int x, int y, QRgb color)
574{
575 setPixel(QPoint(x, y), color);
576}
577
578void BitmapImage::setPixel(QPoint p, QRgb color)
579{
580 setCompositionModeBounds(QRect(p, QSize(1,1)), true, QPainter::CompositionMode_SourceOver);
581 if (mBounds.contains(p))
582 {
583 image()->setPixel(p - mBounds.topLeft(), color);
584 }
585 modification();
586}
587
588void BitmapImage::fillNonAlphaPixels(const QRgb color)
589{
590 if (mBounds.isEmpty()) { return; }
591
592 BitmapImage fill(bounds(), color);
593 paste(&fill, QPainter::CompositionMode_SourceIn);
594}
595
596void BitmapImage::drawLine(QPointF P1, QPointF P2, QPen pen, QPainter::CompositionMode cm, bool antialiasing)
597{
598 int width = 2 + pen.width();
599 setCompositionModeBounds(QRect(P1.toPoint(), P2.toPoint()).normalized().adjusted(-width, -width, width, width), true, cm);
600 if (!image()->isNull())
601 {
602 QPainter painter(image());
603 painter.setCompositionMode(cm);
604 painter.setRenderHint(QPainter::Antialiasing, antialiasing);
605 painter.setPen(pen);
606 painter.drawLine(P1 - mBounds.topLeft(), P2 - mBounds.topLeft());
607 painter.end();
608 }
609 modification();
610}
611
612void BitmapImage::drawRect(QRectF rectangle, QPen pen, QBrush brush, QPainter::CompositionMode cm, bool antialiasing)
613{
614 int width = pen.width();
615 setCompositionModeBounds(rectangle.adjusted(-width, -width, width, width).toRect(), true, cm);
616 if (brush.style() == Qt::RadialGradientPattern)
617 {
618 QRadialGradient* gradient = (QRadialGradient*)brush.gradient();
619 gradient->setCenter(gradient->center() - mBounds.topLeft());
620 gradient->setFocalPoint(gradient->focalPoint() - mBounds.topLeft());
621 }
622 if (!image()->isNull())
623 {
624 QPainter painter(image());
625 painter.setCompositionMode(cm);
626 painter.setRenderHint(QPainter::Antialiasing, antialiasing);
627 painter.setPen(pen);
628 painter.setBrush(brush);
629
630 // Adjust the brush rectangle to be bigger than the bounds itself,
631 // otherwise there will be artifacts shown in some cases when smudging
632 painter.drawRect(rectangle.translated(-mBounds.topLeft()).adjusted(-1, -1, 1, 1));
633 painter.end();
634 }
635 modification();
636}
637
638void BitmapImage::drawEllipse(QRectF rectangle, QPen pen, QBrush brush, QPainter::CompositionMode cm, bool antialiasing)
639{
640 int width = pen.width();
641 setCompositionModeBounds(rectangle.adjusted(-width, -width, width, width).toRect(), true, cm);
642 if (brush.style() == Qt::RadialGradientPattern)
643 {
644 QRadialGradient* gradient = (QRadialGradient*)brush.gradient();
645 gradient->setCenter(gradient->center() - mBounds.topLeft());
646 gradient->setFocalPoint(gradient->focalPoint() - mBounds.topLeft());
647 }
648 if (!image()->isNull())
649 {
650 QPainter painter(image());
651
652 painter.setRenderHint(QPainter::Antialiasing, antialiasing);
653 painter.setPen(pen);
654 painter.setBrush(brush);
655 painter.setCompositionMode(cm);
656 painter.drawEllipse(rectangle.translated(-mBounds.topLeft()));
657 painter.end();
658 }
659 modification();
660}
661
662void BitmapImage::drawPath(QPainterPath path, QPen pen, QBrush brush,
663 QPainter::CompositionMode cm, bool antialiasing)
664{
665 int width = pen.width();
666 // qreal inc = 1.0 + width / 20.0;
667
668 setCompositionModeBounds(path.controlPointRect().adjusted(-width, -width, width, width).toRect(), true, cm);
669
670 if (!image()->isNull())
671 {
672 QPainter painter(image());
673 painter.setCompositionMode(cm);
674 painter.setRenderHint(QPainter::Antialiasing, antialiasing);
675 painter.setPen(pen);
676 painter.setBrush(brush);
677 painter.setTransform(QTransform().translate(-mBounds.left(), -mBounds.top()));
678 painter.setWorldMatrixEnabled(true);
679 if (path.length() > 0)
680 {
681 /*
682 for (int pt = 0; pt < path.elementCount() - 1; pt++)
683 {
684 qreal dx = path.elementAt(pt + 1).x - path.elementAt(pt).x;
685 qreal dy = path.elementAt(pt + 1).y - path.elementAt(pt).y;
686 qreal m = sqrt(dx*dx + dy*dy);
687 qreal factorx = dx / m;
688 qreal factory = dy / m;
689 for (float h = 0.f; h < m; h += inc)
690 {
691 qreal x = path.elementAt(pt).x + factorx * h;
692 qreal y = path.elementAt(pt).y + factory * h;
693 painter.drawPoint(QPointF(x, y));
694 }
695 }
696 */
697 painter.drawPath( path );
698 }
699 else
700 {
701 // forces drawing when points are coincident (mousedown)
702 painter.drawPoint(static_cast<int>(path.elementAt(0).x), static_cast<int>(path.elementAt(0).y));
703 }
704 painter.end();
705 }
706 modification();
707}
708
709BitmapImage* BitmapImage::scanToTransparent(BitmapImage *img, const int threshold, const bool redEnabled, const bool greenEnabled, const bool blueEnabled)
710{
711 Q_ASSERT(img != nullptr);
712
713 QRgb rgba = img->constScanLine(img->left(), img->top());
714 if (qAlpha(rgba) == 0)
715 return img;
716
717 for (int x = img->left(); x <= img->right(); x++)
718 {
719 for (int y = img->top(); y <= img->bottom(); y++)
720 {
721 rgba = img->constScanLine(x, y);
722
723 if (qAlpha(rgba) == 0)
724 break;
725
726 const int grayValue = qGray(rgba);
727 const int redValue = qRed(rgba);
728 const int greenValue = qGreen(rgba);
729 const int blueValue = qBlue(rgba);
730 if (grayValue >= threshold)
731 { // IF Threshold or above
732 img->scanLine(x, y, transp);
733 }
734 else if (redValue > greenValue + COLORDIFF &&
735 redValue > blueValue + COLORDIFF &&
736 redValue > grayValue + GRAYSCALEDIFF)
737 { // IF Red line
738 if (redEnabled)
739 {
740 img->scanLine(x, y, redline);
741 }
742 else
743 {
744 img->scanLine(x, y, transp);
745 }
746 }
747 else if (greenValue > redValue + COLORDIFF &&
748 greenValue > blueValue + COLORDIFF &&
749 greenValue > grayValue + GRAYSCALEDIFF)
750 { // IF Green line
751 if (greenEnabled)
752 {
753 img->scanLine(x, y, greenline);
754 }
755 else
756 {
757 img->scanLine(x, y, transp);
758 }
759 }
760 else if (blueValue > redValue + COLORDIFF &&
761 blueValue > greenValue + COLORDIFF &&
762 blueValue > grayValue + GRAYSCALEDIFF)
763 { // IF Blue line
764 if (blueEnabled)
765 {
766 img->scanLine(x, y, blueline);
767 }
768 else
769 {
770 img->scanLine(x, y, transp);
771 }
772 }
773 else
774 { // okay, so it is in grayscale graduation area
775 if (grayValue >= LOW_THRESHOLD)
776 {
777 const qreal factor = static_cast<qreal>(threshold - grayValue) / static_cast<qreal>(threshold - LOW_THRESHOLD);
778 img->scanLine(x , y, qRgba(0, 0, 0, static_cast<int>(threshold * factor)));
779 }
780 else // grayValue < LOW_THRESHOLD
781 {
782 img->scanLine(x , y, blackline);
783 }
784 }
785 }
786 }
787 img->modification();
788 return img;
789}
790
791Status BitmapImage::writeFile(const QString& filename)
792{
793 DebugDetails dd;
794 dd << "BitmapImage::writeFile";
795 dd << QString("&nbsp;&nbsp;filename = ").append(filename);
796
797 QImageWriter writer(filename);
798 if (!mImage.isNull())
799 {
800 bool b = writer.write(mImage);
801 if (b) {
802 return Status::OK;
803 } else {
804 dd << QString("&nbsp;&nbsp;Error: %1 (Code %2)").arg(writer.errorString()).arg(static_cast<int>(writer.error()));
805 return Status(Status::FAIL, dd);
806 }
807 }
808
809 if (bounds().isEmpty())
810 {
811 QFile f(filename);
812 if(f.exists())
813 {
814 bool b = f.remove();
815 if (!b) {
816 dd << "&nbsp;&nbsp;Error: Image is empty but unable to remove file.";
817 return Status::FAIL;
818 }
819 }
820
821 // The frame is likely empty, act like there's no file name
822 // so we don't end up writing to it later.
823 setFileName("");
824 }
825 return Status::SAFE;
826}
827
828void BitmapImage::clear()
829{
830 mImage = QImage(); // null image
831 mBounds = QRect(0, 0, 0, 0);
832 mMinBound = true;
833 modification();
834}
835
836QRgb BitmapImage::constScanLine(int x, int y) const
837{
838 QRgb result = QRgb();
839 if (mBounds.contains(x, y)) {
840 result = *(reinterpret_cast<const QRgb*>(mImage.constScanLine(y - mBounds.top())) + x - mBounds.left());
841 }
842 return result;
843}
844
845void BitmapImage::scanLine(int x, int y, QRgb color)
846{
847 if (!mBounds.contains(x, y)) {
848 return;
849 }
850 // Make sure color is premultiplied before calling
851 *(reinterpret_cast<QRgb*>(image()->scanLine(y - mBounds.top())) + x - mBounds.left()) = color;
852}
853
854void BitmapImage::clear(QRect rectangle)
855{
856 QRect clearRectangle = mBounds.intersected(rectangle);
857 clearRectangle.moveTopLeft(clearRectangle.topLeft() - mBounds.topLeft());
858
859 setCompositionModeBounds(clearRectangle, true, QPainter::CompositionMode_Clear);
860
861 QPainter painter(image());
862 painter.setCompositionMode(QPainter::CompositionMode_Clear);
863 painter.fillRect(clearRectangle, QColor(0, 0, 0, 0));
864 painter.end();
865
866 modification();
867}
868
869bool BitmapImage::floodFill(BitmapImage** replaceImage,
870 const BitmapImage* targetImage,
871 const QRect& cameraRect,
872 const QPoint& point,
873 const QRgb& fillColor,
874 int tolerance,
875 const int expandValue)
876{
877 // Fill region must be 1 pixel larger than the target image to fill regions on the edge connected only by transparent pixels
878 const QRect& fillBounds = targetImage->mBounds.adjusted(-1, -1, 1, 1);
879 QRect maxBounds = cameraRect.united(fillBounds).adjusted(-expandValue, -expandValue, expandValue, expandValue);
880 const int maxWidth = maxBounds.width(), left = maxBounds.left(), top = maxBounds.top();
881
882 // Square tolerance for use with compareColor
883 tolerance = static_cast<int>(qPow(tolerance, 2));
884
885 QRect newBounds;
886 bool *filledPixels = floodFillPoints(targetImage, maxBounds, point, tolerance, newBounds);
887
888 QRect translatedSearchBounds = newBounds.translated(-maxBounds.topLeft());
889
890 // The scanned bounds should take the expansion into account
891 const QRect& expandRect = newBounds.adjusted(-expandValue, -expandValue, expandValue, expandValue);
892 if (expandValue > 0) {
893 newBounds = expandRect;
894 }
895 if (!maxBounds.contains(newBounds)) {
896 newBounds = maxBounds;
897 }
898 translatedSearchBounds = newBounds.translated(-maxBounds.topLeft());
899
900 if (expandValue > 0) {
901 expandFill(filledPixels, translatedSearchBounds, maxBounds, expandValue);
902 }
903
904 *replaceImage = new BitmapImage(newBounds, Qt::transparent);
905
906 // Fill all the found pixels
907 for (int y = translatedSearchBounds.top(); y <= translatedSearchBounds.bottom(); y++)
908 {
909 for (int x = translatedSearchBounds.left(); x <= translatedSearchBounds.right(); x++)
910 {
911 const int index = y * maxWidth + x;
912 if (!filledPixels[index])
913 {
914 continue;
915 }
916 (*replaceImage)->scanLine(x + left, y + top, fillColor);
917 }
918 }
919
920 delete[] filledPixels;
921
922 return true;
923}
924
925// Flood filling based on this scanline algorithm
926// ----- http://lodev.org/cgtutor/floodfill.html
927bool* BitmapImage::floodFillPoints(const BitmapImage* targetImage,
928 const QRect& searchBounds,
929 QPoint point,
930 const int tolerance,
931 QRect& newBounds)
932{
933 QRgb oldColor = targetImage->constScanLine(point.x(), point.y());
934 oldColor = qRgba(qRed(oldColor), qGreen(oldColor), qBlue(oldColor), qAlpha(oldColor));
935
936 // Preparations
937 QList<QPoint> queue; // queue all the pixels of the filled area (as they are found)
938
939 QPoint tempPoint;
940 QScopedPointer< QHash<QRgb, bool> > cache(new QHash<QRgb, bool>());
941
942 int xTemp = 0;
943 bool spanLeft = false;
944 bool spanRight = false;
945
946 queue.append(point);
947 // Preparations END
948
949 bool *filledPixels = new bool[searchBounds.height()*searchBounds.width()]{};
950
951 BlitRect blitBounds(point);
952 while (!queue.empty())
953 {
954 tempPoint = queue.takeFirst();
955
956 point.setX(tempPoint.x());
957 point.setY(tempPoint.y());
958
959 xTemp = point.x();
960
961 int xCoord = xTemp - searchBounds.left();
962 int yCoord = point.y() - searchBounds.top();
963
964 if (filledPixels[yCoord*searchBounds.width()+xCoord]) continue;
965
966 while (xTemp >= searchBounds.left() &&
967 compareColor(targetImage->constScanLine(xTemp, point.y()), oldColor, tolerance, cache.data())) xTemp--;
968 xTemp++;
969
970 spanLeft = spanRight = false;
971 while (xTemp <= searchBounds.right() &&
972 compareColor(targetImage->constScanLine(xTemp, point.y()), oldColor, tolerance, cache.data()))
973 {
974
975 QPoint floodPoint = QPoint(xTemp, point.y());
976 if (!blitBounds.contains(floodPoint)) {
977 blitBounds.extend(floodPoint);
978 }
979
980 xCoord = xTemp - searchBounds.left();
981 // This pixel is what we're going to fill later
982 filledPixels[yCoord*searchBounds.width()+xCoord] = true;
983
984 if (!spanLeft && (point.y() > searchBounds.top()) &&
985 compareColor(targetImage->constScanLine(xTemp, point.y() - 1), oldColor, tolerance, cache.data())) {
986 queue.append(QPoint(xTemp, point.y() - 1));
987 spanLeft = true;
988 }
989 else if (spanLeft && (point.y() > searchBounds.top()) &&
990 !compareColor(targetImage->constScanLine(xTemp, point.y() - 1), oldColor, tolerance, cache.data())) {
991 spanLeft = false;
992 }
993
994 if (!spanRight && point.y() < searchBounds.bottom() &&
995 compareColor(targetImage->constScanLine(xTemp, point.y() + 1), oldColor, tolerance, cache.data())) {
996 queue.append(QPoint(xTemp, point.y() + 1));
997 spanRight = true;
998 }
999 else if (spanRight && point.y() < searchBounds.bottom() &&
1000 !compareColor(targetImage->constScanLine(xTemp, point.y() + 1), oldColor, tolerance, cache.data())) {
1001 spanRight = false;
1002 }
1003
1004 Q_ASSERT(queue.count() < (searchBounds.width() * searchBounds.height()));
1005 xTemp++;
1006 }
1007 }
1008
1009 newBounds = blitBounds;
1010
1011 return filledPixels;
1012}
1013
1029void BitmapImage::expandFill(bool* fillPixels, const QRect& searchBounds, const QRect& maxBounds, int expand) {
1030
1031 const int maxWidth = maxBounds.width();
1032 const int length = maxBounds.height() * maxBounds.width();
1033
1034 int* manhattanPoints = new int[length]{};
1035
1036 // Fill points with max length, this is important because otherwise the filled pixels will include a border of the expanded area
1037 std::fill_n(manhattanPoints, length, searchBounds.width()+searchBounds.height());
1038
1039 for (int y = searchBounds.top(); y <= searchBounds.bottom(); y++)
1040 {
1041 for (int x = searchBounds.left(); x <= searchBounds.right(); x++)
1042 {
1043 const int index = y*maxWidth+x;
1044
1045 if (fillPixels[index]) {
1046 manhattanPoints[index] = 0;
1047 continue;
1048 }
1049
1050 if (y > searchBounds.top()) {
1051 // the value will be the num of pixels away from y - 1 of the next position
1052 manhattanPoints[index] = qMin(manhattanPoints[index],
1053 manhattanPoints[(y - 1) * maxWidth+x] + 1);
1054
1055 int distance = manhattanPoints[index];
1056 if (distance <= expand) {
1057 fillPixels[index] = true;
1058 }
1059 }
1060 if (x > searchBounds.left()) {
1061 // the value will be the num of pixels away from x - 1 of the next position
1062 manhattanPoints[index] = qMin(manhattanPoints[index],
1063 manhattanPoints[y*maxWidth+(x - 1)] + 1);
1064
1065 int distance = manhattanPoints[index];
1066 if (distance <= expand) {
1067 fillPixels[index] = true;
1068 }
1069 }
1070 }
1071 }
1072
1073 // traverse from bottom right to top left
1074 for (int y = searchBounds.bottom(); y >= searchBounds.top(); y--)
1075 {
1076 for (int x = searchBounds.right(); x >= searchBounds.left(); x--)
1077 {
1078 const int index = y*maxWidth+x;
1079
1080 if (y + 1 < searchBounds.bottom()) {
1081 manhattanPoints[index] = qMin(manhattanPoints[index], manhattanPoints[(y + 1)*maxWidth+x] + 1);
1082
1083 int distance = manhattanPoints[index];
1084 if (distance <= expand) {
1085 fillPixels[index] = true;
1086 }
1087 }
1088 if (x + 1 < searchBounds.right()) {
1089 manhattanPoints[index] = qMin(manhattanPoints[index], manhattanPoints[y*maxWidth+(x + 1)] + 1);
1090
1091 int distance = manhattanPoints[index];
1092 if (distance <= expand) {
1093 fillPixels[index] = true;
1094 }
1095 }
1096 }
1097 }
1098
1099 delete[] manhattanPoints;
1100}
1101
BitmapImage
Definition: bitmapimage.h:28
BitmapImage::updateBounds
void updateBounds(QRect rectangle)
Update image bounds.
Definition: bitmapimage.cpp:300
BitmapImage::loadFile
void loadFile() override
Loads the backing image data from disk and into memory if it exists but hasn't been loaded yet.
Definition: bitmapimage.cpp:130
BitmapImage::mMinBound
bool mMinBound
Definition: bitmapimage.h:197
BitmapImage::isLoaded
bool isLoaded() const override
Checks whether the keyframe holds valid image data.
Definition: bitmapimage.cpp:148
BitmapImage::setCompositionModeBounds
void setCompositionModeBounds(BitmapImage *source, QPainter::CompositionMode cm)
Updates the bounds after a drawImage operation with the composition mode cm.
Definition: bitmapimage.cpp:361
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:1029
BitmapImage::autoCrop
void autoCrop()
Removes any transparent borders by reducing the boundaries.
Definition: bitmapimage.cpp:433
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:156
BitmapImage::unloadFile
void unloadFile() override
Unloads the image data from memory if there's valid backing data on disk and the latest state has bee...
Definition: bitmapimage.cpp:140
BlitRect
Definition: blitrect.h:25
DebugDetails
Definition: pencilerror.h:25
KeyFrame
Definition: keyframe.h:30
Status
Definition: pencilerror.h:40
Tile
Definition: tile.h:24
TiledBuffer
Definition: tiledbuffer.h:50
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::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
QImageWriter
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::append
QString & append(QChar ch)
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 Wed May 20 2026 06:16:32 for Pencil2D by doxygen 1.9.6 based on revision b8a30d7dfe4ff276bcca8087d5396d1507ed94fa