1 #include "widget/wpixmapstore.h"
2 
3 #include <QDir>
4 #include <QString>
5 #include <QtDebug>
6 
7 #include "skin/legacy/imgloader.h"
8 
9 #include "util/math.h"
10 #include "util/memory.h"
11 #include "util/painterscope.h"
12 
13 // static
DrawModeFromString(const QString & str)14 Paintable::DrawMode Paintable::DrawModeFromString(const QString& str) {
15     if (str.compare("FIXED", Qt::CaseInsensitive) == 0) {
16         return FIXED;
17     } else if (str.compare("STRETCH", Qt::CaseInsensitive) == 0) {
18         return STRETCH;
19     } else if (str.compare("STRETCH_ASPECT", Qt::CaseInsensitive) == 0) {
20         return STRETCH_ASPECT;
21     } else if (str.compare("TILE", Qt::CaseInsensitive) == 0) {
22         return TILE;
23     }
24 
25     // Fall back on the implicit default from before Mixxx supported draw modes.
26     qWarning() << "Unknown DrawMode string in DrawModeFromString:"
27                << str << "using FIXED";
28     return FIXED;
29 }
30 
31 // static
DrawModeToString(DrawMode mode)32 QString Paintable::DrawModeToString(DrawMode mode) {
33     switch (mode) {
34         case FIXED:
35             return "FIXED";
36         case STRETCH:
37             return "STRETCH";
38         case STRETCH_ASPECT:
39             return "STRETCH_ASPECT";
40         case TILE:
41             return "TILE";
42     }
43     // Fall back on the implicit default from before Mixxx supported draw modes.
44     qWarning() << "Unknown DrawMode in DrawModeToString " << mode
45                << "using FIXED";
46     return "FIXED";
47 }
48 
Paintable(const PixmapSource & source,DrawMode mode,double scaleFactor)49 Paintable::Paintable(const PixmapSource& source, DrawMode mode, double scaleFactor)
50         : m_drawMode(mode),
51           m_source(source) {
52     if (!source.isSVG()) {
53         m_pPixmap.reset(WPixmapStore::getPixmapNoCache(source.getPath(), scaleFactor));
54     } else {
55         auto pSvg = std::make_unique<QSvgRenderer>();
56         if (!source.getSvgSourceData().isEmpty()) {
57             // Call here the different overload for svg content
58             if (!pSvg->load(source.getSvgSourceData())) {
59                 // The above line already logs a warning
60                 return;
61             }
62         } else if (!source.getPath().isEmpty()) {
63             if (!pSvg->load(source.getPath())) {
64                 // The above line already logs a warning
65                 return;
66             }
67         } else {
68             return;
69         }
70         m_pSvg.reset(pSvg.release());
71 #ifdef __APPLE__
72         // Apple does Retina scaling behind the scenes, so we also pass a
73         // Paintable::FIXED image. On the other targets, it is better to
74         // cache the pixmap. We do not do this for TILE and color schemas.
75         // which can result in a correct but possibly blurry picture at a
76         // Retina display. This can be fixed when switching to QT5
77         if (mode == TILE || WPixmapStore::willCorrectColors()) {
78 #else
79         if (mode == TILE || mode == Paintable::FIXED || WPixmapStore::willCorrectColors()) {
80 #endif
81             // The SVG renderer doesn't directly support tiling, so we render
82             // it to a pixmap which will then get tiled.
83             QImage copy_buffer(m_pSvg->defaultSize() * scaleFactor, QImage::Format_ARGB32);
84             copy_buffer.fill(0x00000000);  // Transparent black.
85             QPainter painter(&copy_buffer);
86             m_pSvg->render(&painter);
87             WPixmapStore::correctImageColors(&copy_buffer);
88 
89             m_pPixmap.reset(new QPixmap(copy_buffer.size()));
90             m_pPixmap->convertFromImage(copy_buffer);
91         }
92     }
93 }
94 
95 bool Paintable::isNull() const {
96     return m_source.isEmpty();
97 }
98 
99 QSize Paintable::size() const {
100     if (!m_pPixmap.isNull()) {
101         return m_pPixmap->size();
102     } else if (!m_pSvg.isNull()) {
103         return m_pSvg->defaultSize();
104     }
105     return QSize();
106 }
107 
108 int Paintable::width() const {
109     if (!m_pPixmap.isNull()) {
110         return m_pPixmap->width();
111     } else if (!m_pSvg.isNull()) {
112         QSize size = m_pSvg->defaultSize();
113         return size.width();
114     }
115     return 0;
116 }
117 
118 int Paintable::height() const {
119     if (!m_pPixmap.isNull()) {
120         return m_pPixmap->height();
121     } else if (!m_pSvg.isNull()) {
122         QSize size = m_pSvg->defaultSize();
123         return size.height();
124     }
125     return 0;
126 }
127 
128 QRectF Paintable::rect() const {
129     if (!m_pPixmap.isNull()) {
130         return m_pPixmap->rect();
131     } else if (!m_pSvg.isNull()) {
132         return QRectF(QPointF(0, 0), m_pSvg->defaultSize());
133     }
134     return QRectF();
135 }
136 
137 void Paintable::draw(const QRectF& targetRect, QPainter* pPainter) {
138     // The sourceRect is implicitly the entire Paintable.
139     draw(targetRect, pPainter, rect());
140 }
141 
142 void Paintable::draw(int x, int y, QPainter* pPainter) {
143     QRectF sourceRect(rect());
144     QRectF targetRect(QPointF(x, y), sourceRect.size());
145     draw(targetRect, pPainter, sourceRect);
146 }
147 
148 void Paintable::draw(const QRectF& targetRect, QPainter* pPainter,
149                      const QRectF& sourceRect) {
150     if (!targetRect.isValid() || !sourceRect.isValid() || isNull()) {
151         return;
152     }
153 
154     switch (m_drawMode) {
155     case FIXED: {
156         // Only render the minimum overlapping rectangle between the source
157         // and target.
158         QSizeF fixedSize(math_min(sourceRect.width(), targetRect.width()),
159                          math_min(sourceRect.height(), targetRect.height()));
160         QRectF adjustedTarget(targetRect.topLeft(), fixedSize);
161         QRectF adjustedSource(sourceRect.topLeft(), fixedSize);
162         drawInternal(adjustedTarget, pPainter, adjustedSource);
163         break;
164     }
165     case STRETCH_ASPECT: {
166         qreal sx = targetRect.width() / sourceRect.width();
167         qreal sy = targetRect.height() / sourceRect.height();
168 
169         // Adjust the scale so that the scaling in both axes is equal.
170         if (sx != sy) {
171             qreal scale = math_min(sx, sy);
172             QRectF adjustedTarget(targetRect.x(),
173                                   targetRect.y(),
174                                   scale * sourceRect.width(),
175                                   scale * sourceRect.height());
176             drawInternal(adjustedTarget, pPainter, sourceRect);
177         } else {
178             drawInternal(targetRect, pPainter, sourceRect);
179         }
180         break;
181     }
182     case STRETCH:
183         drawInternal(targetRect, pPainter, sourceRect);
184         break;
185     case TILE:
186         drawInternal(targetRect, pPainter, sourceRect);
187         break;
188     }
189 }
190 
191 void Paintable::drawCentered(const QRectF& targetRect, QPainter* pPainter,
192                              const QRectF& sourceRect) {
193     switch (m_drawMode) {
194     case FIXED: {
195         // Only render the minimum overlapping rectangle between the source
196         // and target.
197         QSizeF fixedSize(math_min(sourceRect.width(), targetRect.width()),
198                          math_min(sourceRect.height(), targetRect.height()));
199 
200         QRectF adjustedSource(sourceRect.topLeft(), fixedSize);
201         QRectF adjustedTarget(QPointF(-adjustedSource.width() / 2.0,
202                                       -adjustedSource.height() / 2.0),
203                               fixedSize);
204         drawInternal(adjustedTarget, pPainter, adjustedSource);
205         break;
206     }
207     case STRETCH_ASPECT: {
208         qreal sx = targetRect.width() / sourceRect.width();
209         qreal sy = targetRect.height() / sourceRect.height();
210 
211         // Adjust the scale so that the scaling in both axes is equal.
212         if (sx != sy) {
213             qreal scale = math_min(sx, sy);
214             qreal scaledWidth = scale * sourceRect.width();
215             qreal scaledHeight = scale * sourceRect.height();
216             QRectF adjustedTarget(-scaledWidth / 2.0, -scaledHeight / 2.0,
217                                   scaledWidth, scaledHeight);
218             drawInternal(adjustedTarget, pPainter, sourceRect);
219         } else {
220             drawInternal(targetRect, pPainter, sourceRect);
221         }
222         break;
223     }
224     case STRETCH:
225         drawInternal(targetRect, pPainter, sourceRect);
226         break;
227     case TILE:
228         // TODO(XXX): What's the right behavior here? Draw the first tile at the
229         // center point and then tile all around it based on that?
230         drawInternal(targetRect, pPainter, sourceRect);
231         break;
232     }
233 }
234 
235 void Paintable::drawInternal(const QRectF& targetRect, QPainter* pPainter,
236                              const QRectF& sourceRect) {
237     // qDebug() << "Paintable::drawInternal" << DrawModeToString(m_draw_mode)
238     //          << targetRect << sourceRect;
239     if (m_pPixmap) {
240         if (m_drawMode == TILE) {
241             // TODO(rryan): Using a source rectangle doesn't make much sense
242             // with tiling. Ignore the source rect and tile our natural size
243             // across the target rect. What's the right general behavior here?
244             // NOTE(rryan): We round our target/source rectangles to the nearest
245             // pixel for raster images.
246             pPainter->drawTiledPixmap(targetRect.toRect(), *m_pPixmap, QPoint(0,0));
247         } else {
248             // NOTE(rryan): We round our target/source rectangles to the nearest
249             // pixel for raster images.
250             pPainter->drawPixmap(targetRect.toRect(), *m_pPixmap,
251                                  sourceRect.toRect());
252         }
253     } else if (m_pSvg) {
254         if (m_drawMode == TILE) {
255             qWarning() << "Tiled SVG should have been rendered to pixmap!";
256         } else {
257             // NOTE(rryan): QSvgRenderer render does not clip for us -- it
258             // applies a world transformation using viewBox and renders the
259             // entire SVG to the painter. We save/restore the QPainter in case
260             // there is an existing clip region (I don't know of any Mixxx code
261             // that uses one but we may in the future).
262             PainterScope PainterScope(pPainter);
263             pPainter->setClipping(true);
264             pPainter->setClipRect(targetRect);
265             m_pSvg->setViewBox(sourceRect);
266             m_pSvg->render(pPainter, targetRect);
267         }
268     }
269 }
270 
271 // static
272 QString Paintable::getAltFileName(const QString& fileName) {
273     // Detect if the alternate image file exists and, if it does,
274     // return its path instead
275     QStringList temp = fileName.split('.');
276     if (temp.length() != 2) {
277         return fileName;
278     }
279 
280     QString newFileName = temp[0] + QLatin1String("@2x.") + temp[1];
281     QFile file(newFileName);
282     if (QFileInfo(file).exists()) {
283         return newFileName;
284     } else {
285         return fileName;
286     }
287 }
288