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