1 /*
2
3 Pencil2D - Traditional Animation Software
4 Copyright (C) 2005-2007 Patrick Corrieri & Pascal Naidon
5 Copyright (C) 2012-2020 Matthew Chiawen Chang
6
7 This program is free software; you can redistribute it and/or
8 modify it under the terms of the GNU General Public License
9 as published by the Free Software Foundation; version 2 of the License.
10
11 This program is distributed in the hope that it will be useful,
12 but WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 GNU General Public License for more details.
15
16 */
17 #include "bitmapimage.h"
18
19 #include <cmath>
20 #include <QDebug>
21 #include <QtMath>
22 #include <QFile>
23 #include <QPainterPath>
24 #include "util.h"
25
BitmapImage()26 BitmapImage::BitmapImage()
27 {
28 mImage.reset(new QImage); // create null image
29 mBounds = QRect(0, 0, 0, 0);
30 }
31
BitmapImage(const BitmapImage & a)32 BitmapImage::BitmapImage(const BitmapImage& a) : KeyFrame(a)
33 {
34 mBounds = a.mBounds;
35 mMinBound = a.mMinBound;
36 mEnableAutoCrop = a.mEnableAutoCrop;
37 mImage.reset(new QImage(*a.mImage));
38 }
39
BitmapImage(const QRect & rectangle,const QColor & color)40 BitmapImage::BitmapImage(const QRect& rectangle, const QColor& color)
41 {
42 mBounds = rectangle;
43 mImage.reset(new QImage(mBounds.size(), QImage::Format_ARGB32_Premultiplied));
44 mImage->fill(color.rgba());
45 mMinBound = false;
46 }
47
BitmapImage(const QPoint & topLeft,const QImage & image)48 BitmapImage::BitmapImage(const QPoint& topLeft, const QImage& image)
49 {
50 mBounds = QRect(topLeft, image.size());
51 mMinBound = true;
52 mImage.reset(new QImage(image));
53 }
54
BitmapImage(const QPoint & topLeft,const QString & path)55 BitmapImage::BitmapImage(const QPoint& topLeft, const QString& path)
56 {
57 setFileName(path);
58 mImage.reset();
59
60 mBounds = QRect(topLeft, QSize(0, 0));
61 mMinBound = true;
62 setModified(false);
63 }
64
~BitmapImage()65 BitmapImage::~BitmapImage()
66 {
67 }
68
setImage(QImage * img)69 void BitmapImage::setImage(QImage* img)
70 {
71 Q_CHECK_PTR(img);
72 mImage.reset(img);
73 mMinBound = false;
74
75 modification();
76 }
77
operator =(const BitmapImage & a)78 BitmapImage& BitmapImage::operator=(const BitmapImage& a)
79 {
80 if (this == &a)
81 {
82 return *this; // a self-assignment
83 }
84
85 KeyFrame::operator=(a);
86 mBounds = a.mBounds;
87 mMinBound = a.mMinBound;
88 mImage.reset(new QImage(*a.mImage));
89 modification();
90 return *this;
91 }
92
clone()93 BitmapImage* BitmapImage::clone()
94 {
95 return new BitmapImage(*this);
96 }
97
loadFile()98 void BitmapImage::loadFile()
99 {
100 if (mImage == nullptr)
101 {
102 mImage.reset(new QImage(fileName()));
103 mBounds.setSize(mImage->size());
104 mMinBound = false;
105 }
106 }
107
unloadFile()108 void BitmapImage::unloadFile()
109 {
110 if (isModified() == false)
111 {
112 mImage.reset();
113 }
114 }
115
isLoaded()116 bool BitmapImage::isLoaded()
117 {
118 return (mImage != nullptr);
119 }
120
memoryUsage()121 quint64 BitmapImage::memoryUsage()
122 {
123 if (mImage)
124 {
125 return imageSize(*mImage);
126 }
127 return 0;
128 }
129
paintImage(QPainter & painter)130 void BitmapImage::paintImage(QPainter& painter)
131 {
132 painter.drawImage(mBounds.topLeft(), *image());
133 }
134
paintImage(QPainter & painter,QImage & image,QRect sourceRect,QRect destRect)135 void BitmapImage::paintImage(QPainter& painter, QImage& image, QRect sourceRect, QRect destRect)
136 {
137 painter.drawImage(QRect(mBounds.topLeft(), destRect.size()),
138 image,
139 sourceRect);
140 }
141
image()142 QImage* BitmapImage::image()
143 {
144 loadFile();
145 return mImage.get();
146 }
147
copy()148 BitmapImage BitmapImage::copy()
149 {
150 return BitmapImage(mBounds.topLeft(), *image());
151 }
152
copy(QRect rectangle)153 BitmapImage BitmapImage::copy(QRect rectangle)
154 {
155 if (rectangle.isEmpty() || mBounds.isEmpty()) return BitmapImage();
156
157 QRect intersection2 = rectangle.translated(-mBounds.topLeft());
158 BitmapImage result(rectangle.topLeft(), image()->copy(intersection2));
159 return result;
160 }
161
paste(BitmapImage * bitmapImage,QPainter::CompositionMode cm)162 void BitmapImage::paste(BitmapImage* bitmapImage, QPainter::CompositionMode cm)
163 {
164 if(bitmapImage->width() <= 0 || bitmapImage->height() <= 0)
165 {
166 return;
167 }
168
169 setCompositionModeBounds(bitmapImage, cm);
170
171 QImage* image2 = bitmapImage->image();
172
173 QPainter painter(image());
174 painter.setCompositionMode(cm);
175 painter.drawImage(bitmapImage->mBounds.topLeft() - mBounds.topLeft(), *image2);
176 painter.end();
177
178 modification();
179 }
180
moveTopLeft(QPoint point)181 void BitmapImage::moveTopLeft(QPoint point)
182 {
183 mBounds.moveTopLeft(point);
184 // Size is unchanged so there is no need to update mBounds
185 modification();
186 }
187
transform(QRect newBoundaries,bool smoothTransform)188 void BitmapImage::transform(QRect newBoundaries, bool smoothTransform)
189 {
190 mBounds = newBoundaries;
191 newBoundaries.moveTopLeft(QPoint(0, 0));
192 QImage* newImage = new QImage(mBounds.size(), QImage::Format_ARGB32_Premultiplied);
193
194 QPainter painter(newImage);
195 painter.setRenderHint(QPainter::SmoothPixmapTransform, smoothTransform);
196 painter.setCompositionMode(QPainter::CompositionMode_Source);
197 painter.fillRect(newImage->rect(), QColor(0, 0, 0, 0));
198 painter.setCompositionMode(QPainter::CompositionMode_SourceOver);
199 painter.drawImage(newBoundaries, *image());
200 painter.end();
201 mImage.reset(newImage);
202
203 modification();
204 }
205
transformed(QRect selection,QTransform transform,bool smoothTransform)206 BitmapImage BitmapImage::transformed(QRect selection, QTransform transform, bool smoothTransform)
207 {
208 Q_ASSERT(!selection.isEmpty());
209
210 BitmapImage selectedPart = copy(selection);
211
212 // Get the transformed image
213 QImage transformedImage;
214 if (smoothTransform)
215 {
216 transformedImage = selectedPart.image()->transformed(transform, Qt::SmoothTransformation);
217 }
218 else
219 {
220 transformedImage = selectedPart.image()->transformed(transform);
221 }
222 return BitmapImage(transform.mapRect(selection).normalized().topLeft(), transformedImage);
223 }
224
transformed(QRect newBoundaries,bool smoothTransform)225 BitmapImage BitmapImage::transformed(QRect newBoundaries, bool smoothTransform)
226 {
227 BitmapImage transformedImage(newBoundaries, QColor(0, 0, 0, 0));
228 QPainter painter(transformedImage.image());
229 painter.setRenderHint(QPainter::SmoothPixmapTransform, smoothTransform);
230 newBoundaries.moveTopLeft(QPoint(0, 0));
231 painter.drawImage(newBoundaries, *image());
232 painter.end();
233 return transformedImage;
234 }
235
236 /** Update image bounds.
237 *
238 * @param[in] newBoundaries the new bounds
239 *
240 * Sets this image's bounds to rectangle.
241 * Modifies mBounds and crops mImage.
242 */
updateBounds(QRect newBoundaries)243 void BitmapImage::updateBounds(QRect newBoundaries)
244 {
245 // Check to make sure changes actually need to be made
246 if (mBounds == newBoundaries) return;
247
248 QImage* newImage = new QImage( newBoundaries.size(), QImage::Format_ARGB32_Premultiplied);
249 newImage->fill(Qt::transparent);
250 if (!newImage->isNull())
251 {
252 QPainter painter(newImage);
253 painter.drawImage(mBounds.topLeft() - newBoundaries.topLeft(), *mImage);
254 painter.end();
255 }
256 mImage.reset( newImage );
257 mBounds = newBoundaries;
258 mMinBound = false;
259
260 modification();
261 }
262
extend(const QPoint & p)263 void BitmapImage::extend(const QPoint &p)
264 {
265 if (!mBounds.contains(p))
266 {
267 extend(QRect(p, QSize(1, 1)));
268 }
269 }
270
extend(QRect rectangle)271 void BitmapImage::extend(QRect rectangle)
272 {
273 if (rectangle.width() <= 0) rectangle.setWidth(1);
274 if (rectangle.height() <= 0) rectangle.setHeight(1);
275 if (mBounds.contains(rectangle))
276 {
277 // Do nothing
278 }
279 else
280 {
281 QRect newBoundaries = mBounds.united(rectangle).normalized();
282 QImage* newImage = new QImage(newBoundaries.size(), QImage::Format_ARGB32_Premultiplied);
283 newImage->fill(Qt::transparent);
284 if (!newImage->isNull())
285 {
286 QPainter painter(newImage);
287 painter.drawImage(mBounds.topLeft() - newBoundaries.topLeft(), *image());
288 painter.end();
289 }
290 mImage.reset(newImage);
291 mBounds = newBoundaries;
292
293 modification();
294 }
295 }
296
297 /** Updates the bounds after a drawImage operation with the composition mode cm.
298 *
299 * @param[in] source The source image used for the drawImage call.
300 * @param[in] cm The composition mode that will be used for the draw image
301 *
302 * @see BitmapImage::setCompositionModeBounds(BitmapImage, QPainter::CompositionMode)
303 */
setCompositionModeBounds(BitmapImage * source,QPainter::CompositionMode cm)304 void BitmapImage::setCompositionModeBounds(BitmapImage *source, QPainter::CompositionMode cm)
305 {
306 if (source)
307 {
308 setCompositionModeBounds(source->mBounds, source->mMinBound, cm);
309 }
310 }
311
312 /** Updates the bounds after a draw operation with the composition mode cm.
313 *
314 * @param[in] sourceBounds The bounds of the source used for drawcall.
315 * @param[in] isSourceMinBounds Is sourceBounds the minimal bounds for the source image
316 * @param[in] cm The composition mode that will be used for the draw image
317 *
318 * For a call to draw image of a QPainter (initialized with mImage) with an argument
319 * of source, this function intelligently calculates the bounds. It will attempt to
320 * preserve minimum bounds based on the composition mode.
321 *
322 * This works baed on the principle that some minimal bounds can be determined
323 * solely by the minimal bounds of this and source, depending on the value of cm.
324 * Some composition modes only expand, or have no affect on the bounds.
325 *
326 * @warning The draw operation described by the arguments of this
327 * function needs to be called after this function is run,
328 * or the bounds will be out of sync. If mBounds is null,
329 * no draw operation needs to be performed.
330 */
setCompositionModeBounds(QRect sourceBounds,bool isSourceMinBounds,QPainter::CompositionMode cm)331 void BitmapImage::setCompositionModeBounds(QRect sourceBounds, bool isSourceMinBounds, QPainter::CompositionMode cm)
332 {
333 QRect newBoundaries;
334 switch(cm)
335 {
336 case QPainter::CompositionMode_Destination:
337 case QPainter::CompositionMode_SourceAtop:
338 // The Destination and SourceAtop modes
339 // do not change the bounds from destination.
340 newBoundaries = mBounds;
341 // mMinBound remains the same
342 break;
343 case QPainter::CompositionMode_SourceIn:
344 case QPainter::CompositionMode_DestinationIn:
345 case QPainter::CompositionMode_Clear:
346 case QPainter::CompositionMode_DestinationOut:
347 // The bounds of the result of SourceIn, DestinationIn, Clear, and DestinationOut
348 // modes are no larger than the destination bounds
349 newBoundaries = mBounds;
350 mMinBound = false;
351 break;
352 default:
353 // If it's not one of the above cases, create a union of the two bounds.
354 // This contains the minimum bounds, if both the destination and source
355 // use their respective minimum bounds.
356 newBoundaries = mBounds.united(sourceBounds);
357 mMinBound = mMinBound && isSourceMinBounds;
358 }
359
360 updateBounds(newBoundaries);
361 }
362
363 /** Removes any transparent borders by reducing the boundaries.
364 *
365 * This function reduces the bounds of an image until the top and
366 * bottom rows, and the left and right columns of pixels each
367 * contain at least one pixel with a non-zero alpha value
368 * (i.e. non-transparent pixel). Both mBounds and
369 * the size of #mImage are updated.
370 *
371 * @pre mBounds.size() == mImage->size()
372 * @post Either the first and last rows and columns all contain a
373 * pixel with alpha > 0 or mBounds.isEmpty() == true
374 * @post isMinimallyBounded() == true
375 */
autoCrop()376 void BitmapImage::autoCrop()
377 {
378 if (!mEnableAutoCrop) return;
379 if (mBounds.isEmpty()) return; // Exit if current bounds are null
380 if (!mImage) return;
381
382 Q_ASSERT(mBounds.size() == mImage->size());
383
384 // Exit if already min bounded
385 if (mMinBound) return;
386
387 // Get image properties
388 const int width = mImage->width();
389
390 // Relative top and bottom row indices (inclusive)
391 int relTop = 0;
392 int relBottom = mBounds.height()-1;
393
394 // Check top row
395 bool isEmpty = true; // Used to track if a non-transparent pixel has been found
396 while (isEmpty && relTop <= relBottom) // Loop through rows
397 {
398 // Point cursor to the first pixel in the current top row
399 const QRgb* cursor = reinterpret_cast<const QRgb*>(mImage->constScanLine(relTop));
400 for (int col = 0; col < width; col++) // Loop through pixels in row
401 {
402 // If the pixel is not transparent
403 // (i.e. alpha channel > 0)
404 if (qAlpha(*cursor) != 0)
405 {
406 // We've found a non-transparent pixel in row relTop,
407 // so we can stop looking for one
408 isEmpty = false;
409 break;
410 }
411 // Move cursor to point to the next pixel in the row
412 cursor++;
413 }
414 if (isEmpty)
415 {
416 // If the row we just checked was empty, increase relTop
417 // to remove the empty row from the top of the bounding box
418 ++relTop;
419 }
420 }
421
422 // Check bottom row
423 isEmpty = true; // Reset isEmpty
424 while (isEmpty && relBottom >= relTop) // Loop through rows
425 {
426 // Point cursor to the first pixel in the current bottom row
427 const QRgb* cursor = reinterpret_cast<const QRgb*>(mImage->constScanLine(relBottom));
428 for (int col = 0; col < width; col++) // Loop through pixels in row
429 {
430 // If the pixel is not transparent
431 // (i.e. alpha channel > 0)
432 if(qAlpha(*cursor) != 0)
433 {
434 // We've found a non-transparent pixel in row relBottom,
435 // so we can stop looking for one
436 isEmpty = false;
437 break;
438 }
439 // Move cursor to point to the next pixel in the row
440 ++cursor;
441 }
442 if (isEmpty)
443 {
444 // If the row we just checked was empty, decrease relBottom
445 // to remove the empty row from the bottom of the bounding box
446 --relBottom;
447 }
448 }
449
450 // Relative left and right column indices (inclusive)
451 int relLeft = 0;
452 int relRight = mBounds.width()-1;
453
454 // Check left row
455 isEmpty = (relBottom >= relTop); // Check left only when
456 while (isEmpty && relBottom >= relTop && relLeft <= relRight) // Loop through columns
457 {
458 // Point cursor to the pixel at row relTop and column relLeft
459 const QRgb* cursor = reinterpret_cast<const QRgb*>(mImage->constScanLine(relTop)) + relLeft;
460 // Loop through pixels in column
461 // Note: we only need to loop from relTop to relBottom (inclusive)
462 // not the full image height, because rows 0 to relTop-1 and
463 // relBottom+1 to mBounds.height() have already been
464 // confirmed to contain only transparent pixels
465 for (int row = relTop; row <= relBottom; row++)
466 {
467 // If the pixel is not transparent
468 // (i.e. alpha channel > 0)
469 if(qAlpha(*cursor) != 0)
470 {
471 // We've found a non-transparent pixel in column relLeft,
472 // so we can stop looking for one
473 isEmpty = false;
474 break;
475 }
476 // Move cursor to point to next pixel in the column
477 // Increment by width because the data is in row-major order
478 cursor += width;
479 }
480 if (isEmpty)
481 {
482 // If the column we just checked was empty, increase relLeft
483 // to remove the empty column from the left of the bounding box
484 ++relLeft;
485 }
486 }
487
488 // Check right row
489 isEmpty = (relBottom >= relTop); // Reset isEmpty
490 while (isEmpty && relRight >= relLeft) // Loop through columns
491 {
492 // Point cursor to the pixel at row relTop and column relRight
493 const QRgb* cursor = reinterpret_cast<const QRgb*>(mImage->constScanLine(relTop)) + relRight;
494 // Loop through pixels in column
495 // Note: we only need to loop from relTop to relBottom (inclusive)
496 // not the full image height, because rows 0 to relTop-1 and
497 // relBottom+1 to mBounds.height()-1 have already been
498 // confirmed to contain only transparent pixels
499 for (int row = relTop; row <= relBottom; row++)
500 {
501 // If the pixel is not transparent
502 // (i.e. alpha channel > 0)
503 if(qAlpha(*cursor) != 0)
504 {
505 // We've found a non-transparent pixel in column relRight,
506 // so we can stop looking for one
507 isEmpty = false;
508 break;
509 }
510 // Move cursor to point to next pixel in the column
511 // Increment by width because the data is in row-major order
512 cursor += width;
513 }
514 if (isEmpty)
515 {
516 // If the column we just checked was empty, increase relRight
517 // to remove the empty column from the left of the bounding box
518 --relRight;
519 }
520 }
521
522 //qDebug() << "Original" << mBounds;
523 //qDebug() << "Autocrop" << relLeft << relTop << relRight - mBounds.width() + 1 << relBottom - mBounds.height() + 1;
524 // Update mBounds and mImage if necessary
525 updateBounds(mBounds.adjusted(relLeft, relTop, relRight - mBounds.width() + 1, relBottom - mBounds.height() + 1));
526
527 //qDebug() << "New bounds" << mBounds;
528
529 mMinBound = true;
530 }
531
pixel(int x,int y)532 QRgb BitmapImage::pixel(int x, int y)
533 {
534 return pixel(QPoint(x, y));
535 }
536
pixel(QPoint p)537 QRgb BitmapImage::pixel(QPoint p)
538 {
539 QRgb result = qRgba(0, 0, 0, 0); // black
540 if (mBounds.contains(p))
541 result = image()->pixel(p - mBounds.topLeft());
542 return result;
543 }
544
setPixel(int x,int y,QRgb color)545 void BitmapImage::setPixel(int x, int y, QRgb color)
546 {
547 setPixel(QPoint(x, y), color);
548 }
549
setPixel(QPoint p,QRgb color)550 void BitmapImage::setPixel(QPoint p, QRgb color)
551 {
552 setCompositionModeBounds(QRect(p, QSize(1,1)), true, QPainter::CompositionMode_SourceOver);
553 if (mBounds.contains(p))
554 {
555 image()->setPixel(p - mBounds.topLeft(), color);
556 }
557 modification();
558 }
559
fillNonAlphaPixels(const QRgb color)560 void BitmapImage::fillNonAlphaPixels(const QRgb color)
561 {
562 if (mBounds.isEmpty()) { return; }
563
564 BitmapImage fill(bounds(), color);
565 paste(&fill, QPainter::CompositionMode_SourceIn);
566 }
567
drawLine(QPointF P1,QPointF P2,QPen pen,QPainter::CompositionMode cm,bool antialiasing)568 void BitmapImage::drawLine(QPointF P1, QPointF P2, QPen pen, QPainter::CompositionMode cm, bool antialiasing)
569 {
570 int width = 2 + pen.width();
571 setCompositionModeBounds(QRect(P1.toPoint(), P2.toPoint()).normalized().adjusted(-width, -width, width, width), true, cm);
572 if (!image()->isNull())
573 {
574 QPainter painter(image());
575 painter.setCompositionMode(cm);
576 painter.setRenderHint(QPainter::Antialiasing, antialiasing);
577 painter.setPen(pen);
578 painter.drawLine(P1 - mBounds.topLeft(), P2 - mBounds.topLeft());
579 painter.end();
580 }
581 modification();
582 }
583
drawRect(QRectF rectangle,QPen pen,QBrush brush,QPainter::CompositionMode cm,bool antialiasing)584 void BitmapImage::drawRect(QRectF rectangle, QPen pen, QBrush brush, QPainter::CompositionMode cm, bool antialiasing)
585 {
586 int width = pen.width();
587 setCompositionModeBounds(rectangle.adjusted(-width, -width, width, width).toRect(), true, cm);
588 if (brush.style() == Qt::RadialGradientPattern)
589 {
590 QRadialGradient* gradient = (QRadialGradient*)brush.gradient();
591 gradient->setCenter(gradient->center() - mBounds.topLeft());
592 gradient->setFocalPoint(gradient->focalPoint() - mBounds.topLeft());
593 }
594 if (!image()->isNull())
595 {
596 QPainter painter(image());
597 painter.setCompositionMode(cm);
598 painter.setRenderHint(QPainter::Antialiasing, antialiasing);
599 painter.setPen(pen);
600 painter.setBrush(brush);
601 painter.drawRect(rectangle.translated(-mBounds.topLeft()));
602 painter.end();
603 }
604 modification();
605 }
606
drawEllipse(QRectF rectangle,QPen pen,QBrush brush,QPainter::CompositionMode cm,bool antialiasing)607 void BitmapImage::drawEllipse(QRectF rectangle, QPen pen, QBrush brush, QPainter::CompositionMode cm, bool antialiasing)
608 {
609 int width = pen.width();
610 setCompositionModeBounds(rectangle.adjusted(-width, -width, width, width).toRect(), true, cm);
611 if (brush.style() == Qt::RadialGradientPattern)
612 {
613 QRadialGradient* gradient = (QRadialGradient*)brush.gradient();
614 gradient->setCenter(gradient->center() - mBounds.topLeft());
615 gradient->setFocalPoint(gradient->focalPoint() - mBounds.topLeft());
616 }
617 if (!image()->isNull())
618 {
619 QPainter painter(image());
620
621 painter.setRenderHint(QPainter::Antialiasing, antialiasing);
622 painter.setPen(pen);
623 painter.setBrush(brush);
624 painter.setCompositionMode(cm);
625 painter.drawEllipse(rectangle.translated(-mBounds.topLeft()));
626 painter.end();
627 }
628 modification();
629 }
630
drawPath(QPainterPath path,QPen pen,QBrush brush,QPainter::CompositionMode cm,bool antialiasing)631 void BitmapImage::drawPath(QPainterPath path, QPen pen, QBrush brush,
632 QPainter::CompositionMode cm, bool antialiasing)
633 {
634 int width = pen.width();
635 // qreal inc = 1.0 + width / 20.0;
636
637 setCompositionModeBounds(path.controlPointRect().adjusted(-width, -width, width, width).toRect(), true, cm);
638
639 if (!image()->isNull())
640 {
641 QPainter painter(image());
642 painter.setCompositionMode(cm);
643 painter.setRenderHint(QPainter::Antialiasing, antialiasing);
644 painter.setPen(pen);
645 painter.setBrush(brush);
646 painter.setTransform(QTransform().translate(-mBounds.left(), -mBounds.top()));
647 painter.setWorldMatrixEnabled(true);
648 if (path.length() > 0)
649 {
650 /*
651 for (int pt = 0; pt < path.elementCount() - 1; pt++)
652 {
653 qreal dx = path.elementAt(pt + 1).x - path.elementAt(pt).x;
654 qreal dy = path.elementAt(pt + 1).y - path.elementAt(pt).y;
655 qreal m = sqrt(dx*dx + dy*dy);
656 qreal factorx = dx / m;
657 qreal factory = dy / m;
658 for (float h = 0.f; h < m; h += inc)
659 {
660 qreal x = path.elementAt(pt).x + factorx * h;
661 qreal y = path.elementAt(pt).y + factory * h;
662 painter.drawPoint(QPointF(x, y));
663 }
664 }
665 */
666 painter.drawPath( path );
667 }
668 else
669 {
670 // forces drawing when points are coincident (mousedown)
671 painter.drawPoint(static_cast<int>(path.elementAt(0).x), static_cast<int>(path.elementAt(0).y));
672 }
673 painter.end();
674 }
675 modification();
676 }
677
findLeft(QRectF rect,int grayValue)678 PegbarResult BitmapImage::findLeft(QRectF rect, int grayValue)
679 {
680 PegbarResult result;
681 result.value = -1;
682 result.errorcode = Status::FAIL;
683 int left = static_cast<int>(rect.left());
684 int right = static_cast<int>(rect.right());
685 int top = static_cast<int>(rect.top());
686 int bottom = static_cast<int>(rect.bottom());
687 for (int x = left; x <= right; x++)
688 {
689 for (int y = top; y <= bottom; y++)
690 {
691 if (qAlpha(constScanLine(x,y)) == 255 && qGray(constScanLine(x,y)) < grayValue)
692 {
693 result.value = x;
694 result.errorcode = Status::OK;
695 return result;
696 }
697 }
698 }
699 return result;
700 }
701
findTop(QRectF rect,int grayValue)702 PegbarResult BitmapImage::findTop(QRectF rect, int grayValue)
703 {
704 PegbarResult result;
705 result.value = -1;
706 result.errorcode = Status::FAIL;
707 int left = static_cast<int>(rect.left());
708 int right = static_cast<int>(rect.right());
709 int top = static_cast<int>(rect.top());
710 int bottom = static_cast<int>(rect.bottom());
711 for (int y = top; y <= bottom; y++)
712 {
713 for (int x = left; x <= right; x++)
714 {
715 if (qAlpha(constScanLine(x,y)) == 255 && qGray(constScanLine(x,y)) < grayValue)
716 {
717 result.value = y;
718 result.errorcode = Status::OK;
719 return result;
720 }
721 }
722 }
723 return result;
724 }
725
writeFile(const QString & filename)726 Status BitmapImage::writeFile(const QString& filename)
727 {
728 if (mImage && !mImage->isNull())
729 {
730 bool b = mImage->save(filename);
731 return (b) ? Status::OK : Status::FAIL;
732 }
733
734 if (bounds().isEmpty())
735 {
736 QFile f(filename);
737 if(f.exists())
738 {
739 bool b = f.remove();
740 return (b) ? Status::OK : Status::FAIL;
741 }
742 return Status::SAFE;
743 }
744 return Status::SAFE;
745 }
746
clear()747 void BitmapImage::clear()
748 {
749 mImage.reset(new QImage); // null image
750 mBounds = QRect(0, 0, 0, 0);
751 mMinBound = true;
752 modification();
753 }
754
constScanLine(int x,int y) const755 QRgb BitmapImage::constScanLine(int x, int y) const
756 {
757 QRgb result = qRgba(0, 0, 0, 0);
758 if (mBounds.contains(QPoint(x, y)))
759 {
760 result = *(reinterpret_cast<const QRgb*>(mImage->constScanLine(y - mBounds.top())) + x - mBounds.left());
761 }
762 return result;
763 }
764
scanLine(int x,int y,QRgb color)765 void BitmapImage::scanLine(int x, int y, QRgb color)
766 {
767 extend(QPoint(x, y));
768 if (mBounds.contains(QPoint(x, y)))
769 {
770 // Make sure color is premultiplied before calling
771 *(reinterpret_cast<QRgb*>(image()->scanLine(y - mBounds.top())) + x - mBounds.left()) =
772 qRgba(
773 qRed(color),
774 qGreen(color),
775 qBlue(color),
776 qAlpha(color));
777 }
778 }
779
clear(QRect rectangle)780 void BitmapImage::clear(QRect rectangle)
781 {
782 QRect clearRectangle = mBounds.intersected(rectangle);
783 clearRectangle.moveTopLeft(clearRectangle.topLeft() - mBounds.topLeft());
784
785 setCompositionModeBounds(clearRectangle, true, QPainter::CompositionMode_Clear);
786
787 QPainter painter(image());
788 painter.setCompositionMode(QPainter::CompositionMode_Clear);
789 painter.fillRect(clearRectangle, QColor(0, 0, 0, 0));
790 painter.end();
791
792 modification();
793 }
794
795 /** Compare colors for the purposes of flood filling
796 *
797 * Calculates the Eulcidian difference of the RGB channels
798 * of the image and compares it to the tolerance
799 *
800 * @param[in] newColor The first color to compare
801 * @param[in] oldColor The second color to compare
802 * @param[in] tolerance The threshold limit between a matching and non-matching color
803 * @param[in,out] cache Contains a mapping of previous results of compareColor with rule that
804 * cache[someColor] = compareColor(someColor, oldColor, tolerance)
805 *
806 * @return Returns true if the colors have a similarity below the tolerance level
807 * (i.e. if Eulcidian distance squared is <= tolerance)
808 */
compareColor(QRgb newColor,QRgb oldColor,int tolerance,QHash<QRgb,bool> * cache)809 bool BitmapImage::compareColor(QRgb newColor, QRgb oldColor, int tolerance, QHash<QRgb, bool> *cache)
810 {
811 // Handle trivial case
812 if (newColor == oldColor) return true;
813
814 if(cache && cache->contains(newColor)) return cache->value(newColor);
815
816 // Get Eulcidian distance between colors
817 // Not an accurate representation of human perception,
818 // but it's the best any image editing program ever does
819 int diffRed = static_cast<int>(qPow(qRed(oldColor) - qRed(newColor), 2));
820 int diffGreen = static_cast<int>(qPow(qGreen(oldColor) - qGreen(newColor), 2));
821 int diffBlue = static_cast<int>(qPow(qBlue(oldColor) - qBlue(newColor), 2));
822 // This may not be the best way to handle alpha since the other channels become less relevant as
823 // the alpha is reduces (ex. QColor(0,0,0,0) is the same as QColor(255,255,255,0))
824 int diffAlpha = static_cast<int>(qPow(qAlpha(oldColor) - qAlpha(newColor), 2));
825
826 bool isSimilar = (diffRed + diffGreen + diffBlue + diffAlpha) <= tolerance;
827
828 if(cache)
829 {
830 Q_ASSERT(cache->contains(isSimilar) ? isSimilar == (*cache)[newColor] : true);
831 (*cache)[newColor] = isSimilar;
832 }
833
834 return isSimilar;
835 }
836
837 // Flood fill
838 // ----- http://lodev.org/cgtutor/floodfill.html
floodFill(BitmapImage * targetImage,QRect cameraRect,QPoint point,QRgb newColor,int tolerance)839 void BitmapImage::floodFill(BitmapImage* targetImage,
840 QRect cameraRect,
841 QPoint point,
842 QRgb newColor,
843 int tolerance)
844 {
845 // If the point we are supposed to fill is outside the image and camera bounds, do nothing
846 if(!cameraRect.united(targetImage->bounds()).contains(point))
847 {
848 return;
849 }
850
851 // Square tolerance for use with compareColor
852 tolerance = static_cast<int>(qPow(tolerance, 2));
853
854 QRgb oldColor = targetImage->pixel(point);
855 oldColor = qRgba(qRed(oldColor), qGreen(oldColor), qBlue(oldColor), qAlpha(oldColor));
856
857 // Preparations
858 QList<QPoint> queue; // queue all the pixels of the filled area (as they are found)
859
860 BitmapImage* replaceImage = nullptr;
861 QPoint tempPoint;
862 QRgb newPlacedColor = 0;
863 QScopedPointer< QHash<QRgb, bool> > cache(new QHash<QRgb, bool>());
864
865 int xTemp = 0;
866 bool spanLeft = false;
867 bool spanRight = false;
868
869 // Extend to size of Camera
870 targetImage->extend(cameraRect);
871 replaceImage = new BitmapImage(targetImage->mBounds, Qt::transparent);
872
873 queue.append(point);
874 // Preparations END
875
876 while (!queue.empty())
877 {
878 tempPoint = queue.takeFirst();
879
880 point.setX(tempPoint.x());
881 point.setY(tempPoint.y());
882
883 xTemp = point.x();
884
885 newPlacedColor = replaceImage->constScanLine(xTemp, point.y());
886 while (xTemp >= targetImage->mBounds.left() &&
887 compareColor(targetImage->constScanLine(xTemp, point.y()), oldColor, tolerance, cache.data())) xTemp--;
888 xTemp++;
889
890 spanLeft = spanRight = false;
891 while (xTemp <= targetImage->mBounds.right() &&
892 compareColor(targetImage->constScanLine(xTemp, point.y()), oldColor, tolerance, cache.data()) &&
893 newPlacedColor != newColor)
894 {
895
896 // Set pixel color
897 replaceImage->scanLine(xTemp, point.y(), newColor);
898
899 if (!spanLeft && (point.y() > targetImage->mBounds.top()) &&
900 compareColor(targetImage->constScanLine(xTemp, point.y() - 1), oldColor, tolerance, cache.data())) {
901 queue.append(QPoint(xTemp, point.y() - 1));
902 spanLeft = true;
903 }
904 else if (spanLeft && (point.y() > targetImage->mBounds.top()) &&
905 !compareColor(targetImage->constScanLine(xTemp, point.y() - 1), oldColor, tolerance, cache.data())) {
906 spanLeft = false;
907 }
908
909 if (!spanRight && point.y() < targetImage->mBounds.bottom() &&
910 compareColor(targetImage->constScanLine(xTemp, point.y() + 1), oldColor, tolerance, cache.data())) {
911 queue.append(QPoint(xTemp, point.y() + 1));
912 spanRight = true;
913
914 }
915 else if (spanRight && point.y() < targetImage->mBounds.bottom() &&
916 !compareColor(targetImage->constScanLine(xTemp, point.y() + 1), oldColor, tolerance, cache.data())) {
917 spanRight = false;
918 }
919
920 Q_ASSERT(queue.count() < (targetImage->mBounds.width() * targetImage->mBounds.height()));
921 xTemp++;
922 }
923 }
924
925 targetImage->paste(replaceImage);
926 targetImage->modification();
927 delete replaceImage;
928 }
929