1 /*
2     SPDX-FileCopyrightText: 1997 Mathias Mueller <in5y158@public.uni-hamburg.de>
3     SPDX-FileCopyrightText: 2006 Mauricio Piacentini <mauricio@tabuleiro.com>
4 
5     SPDX-License-Identifier: GPL-2.0-or-later
6 */
7 
8 // own
9 #include "kmahjonggtileset.h"
10 
11 // STL
12 #include <cstdlib>
13 
14 // Qt
15 #include <QFile>
16 #include <QGuiApplication>
17 #include <QImage>
18 #include <QMap>
19 #include <QPainter>
20 #include <QPixmapCache>
21 #include <QStandardPaths>
22 #include <QSvgRenderer>
23 
24 // KF
25 #include <KConfig>
26 #include <KConfigGroup>
27 #include <KLocalizedString>
28 
29 // LibKMahjongg
30 #include "libkmahjongg_debug.h"
31 
32 class KMahjonggTilesetMetricsData
33 {
34 public:
35     short lvloffx; // used for 3D indentation, x value
36     short lvloffy; // used for 3D indentation, y value
37     short w; // tile width ( +border +shadow)
38     short h; // tile height ( +border +shadow)
39     short fw; // face width
40     short fh; // face height
41 
KMahjonggTilesetMetricsData()42     KMahjonggTilesetMetricsData()
43         : lvloffx(0)
44         , lvloffy(0)
45         , w(0)
46         , h(0)
47         , fw(0)
48         , fh(0)
49     {
50     }
51 };
52 
53 class KMahjonggTilesetPrivate
54 {
55 public:
KMahjonggTilesetPrivate()56     KMahjonggTilesetPrivate()
57         : isSVG(false)
58         , graphicsLoaded(false)
59     {
60     }
61     QList<QString> elementIdTable;
62     QMap<QString, QString> authorproperties;
63 
64     KMahjonggTilesetMetricsData originaldata;
65     KMahjonggTilesetMetricsData scaleddata;
66     QString filename; // cache the last file loaded to save reloading it
67     QString graphicspath;
68 
69     QSvgRenderer svg;
70     bool isSVG;
71     bool graphicsLoaded;
72 };
73 
74 // ---------------------------------------------------------
75 
KMahjonggTileset()76 KMahjonggTileset::KMahjonggTileset()
77     : d(new KMahjonggTilesetPrivate)
78 {
79     buildElementIdTable();
80 
81     static bool _inited = false;
82     if (_inited) {
83         return;
84     }
85     _inited = true;
86 }
87 
88 // ---------------------------------------------------------
89 
90 KMahjonggTileset::~KMahjonggTileset() = default;
91 
updateScaleInfo(short tilew,short tileh)92 void KMahjonggTileset::updateScaleInfo(short tilew, short tileh)
93 {
94     d->scaleddata.w = tilew;
95     d->scaleddata.h = tileh;
96     double ratio = (static_cast<qreal>(d->scaleddata.w)) / (static_cast<qreal>(d->originaldata.w));
97     d->scaleddata.lvloffx = static_cast<short>(d->originaldata.lvloffx * ratio);
98     d->scaleddata.lvloffy = static_cast<short>(d->originaldata.lvloffy * ratio);
99     d->scaleddata.fw = static_cast<short>(d->originaldata.fw * ratio);
100     d->scaleddata.fh = static_cast<short>(d->originaldata.fh * ratio);
101 }
102 
preferredTileSize(const QSize & boardsize,int horizontalCells,int verticalCells)103 QSize KMahjonggTileset::preferredTileSize(const QSize & boardsize, int horizontalCells, int verticalCells)
104 {
105     //calculate our best tile size to fit the boardsize passed to us
106     qreal newtilew, newtileh, aspectratio;
107     qreal bw = boardsize.width();
108     qreal bh = boardsize.height();
109 
110     //use tileface for calculation, with one complete tile in the sum for extra margin
111     qreal fullh = (d->originaldata.fh * verticalCells) + d->originaldata.h;
112     qreal fullw = (d->originaldata.fw * horizontalCells) + d->originaldata.w;
113     qreal floatw = d->originaldata.w;
114     qreal floath = d->originaldata.h;
115 
116     if ((fullw / fullh) > (bw / bh)) {
117         //space will be left on height, use width as limit
118         aspectratio = bw / fullw;
119     } else {
120         aspectratio = bh / fullh;
121     }
122     newtilew = aspectratio * floatw;
123     newtileh = aspectratio * floath;
124     return QSize(static_cast<short>(newtilew), static_cast<short>(newtileh));
125 }
126 
loadDefault()127 bool KMahjonggTileset::loadDefault()
128 {
129     QString idx = QStringLiteral("default.desktop");
130 
131     QString tilesetPath = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("kmahjongglib/tilesets/") + idx);
132     qCDebug(LIBKMAHJONGG_LOG) << "Inside LoadDefault(), located path at" << tilesetPath;
133     if (tilesetPath.isEmpty()) {
134         return false;
135     }
136     return loadTileset(tilesetPath);
137 }
138 
authorProperty(const QString & key) const139 QString KMahjonggTileset::authorProperty(const QString & key) const
140 {
141     return d->authorproperties[key];
142 }
143 
width() const144 short KMahjonggTileset::width() const
145 {
146     return d->scaleddata.w;
147 }
148 
height() const149 short KMahjonggTileset::height() const
150 {
151     return d->scaleddata.h;
152 }
153 
levelOffsetX() const154 short KMahjonggTileset::levelOffsetX() const
155 {
156     return d->scaleddata.lvloffx;
157 }
158 
levelOffsetY() const159 short KMahjonggTileset::levelOffsetY() const
160 {
161     return d->scaleddata.lvloffy;
162 }
163 
qWidth() const164 short KMahjonggTileset::qWidth() const
165 {
166     return static_cast<short>(d->scaleddata.fw / 2.0);
167 }
168 
qHeight() const169 short KMahjonggTileset::qHeight() const
170 {
171     return static_cast<short>(d->scaleddata.fh / 2.0);
172 }
173 
path() const174 QString KMahjonggTileset::path() const
175 {
176     return d->filename;
177 }
178 
179 #define kTilesetVersionFormat 1
180 
181 // ---------------------------------------------------------
loadTileset(const QString & tilesetPath)182 bool KMahjonggTileset::loadTileset(const QString & tilesetPath)
183 {
184     //qCDebug(LIBKMAHJONGG_LOG) << "Attempting to load .desktop at" << tilesetPath;
185 
186     //clear our properties map
187     d->authorproperties.clear();
188 
189     // verify if it is a valid file first and if we can open it
190     QFile tilesetfile(tilesetPath);
191     if (!tilesetfile.open(QIODevice::ReadOnly)) {
192         return false;
193     }
194     tilesetfile.close();
195 
196     KConfig tileconfig(tilesetPath, KConfig::SimpleConfig);
197     KConfigGroup group = tileconfig.group("KMahjonggTileset");
198 
199     d->authorproperties.insert(QStringLiteral("Name"), group.readEntry("Name")); // Returns translated data
200     d->authorproperties.insert(QStringLiteral("Author"), group.readEntry("Author"));
201     d->authorproperties.insert(QStringLiteral("Description"), group.readEntry("Description"));
202     d->authorproperties.insert(QStringLiteral("AuthorEmail"), group.readEntry("AuthorEmail"));
203 
204     //Version control
205     int tileversion = group.readEntry("VersionFormat", 0);
206     //Format is increased when we have incompatible changes, meaning that older clients are not able to use the remaining information safely
207     if (tileversion > kTilesetVersionFormat) {
208         return false;
209     }
210 
211     QString graphName = group.readEntry("FileName");
212 
213     d->graphicspath = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("kmahjongglib/tilesets/") + graphName);
214     //qCDebug(LIBKMAHJONGG_LOG) << "Using tileset at" << d->graphicspath;
215 
216     //only SVG for now
217     d->isSVG = true;
218     if (d->graphicspath.isEmpty()) {
219         return false;
220     }
221 
222     d->originaldata.w = group.readEntry("TileWidth", 30);
223     d->originaldata.h = group.readEntry("TileHeight", 50);
224     d->originaldata.fw = group.readEntry("TileFaceWidth", 30);
225     d->originaldata.fh = group.readEntry("TileFaceHeight", 50);
226     d->originaldata.lvloffx = group.readEntry("LevelOffsetX", 10);
227     d->originaldata.lvloffy = group.readEntry("LevelOffsetY", 10);
228 
229     //client application needs to call loadGraphics()
230     d->graphicsLoaded = false;
231     d->filename = tilesetPath;
232 
233     return true;
234 }
235 
236 // ---------------------------------------------------------
loadGraphics()237 bool KMahjonggTileset::loadGraphics()
238 {
239     if (d->graphicsLoaded) {
240         return true;
241     }
242     if (d->isSVG) {
243         //really?
244         d->svg.load(d->graphicspath);
245         if (d->svg.isValid()) {
246             //invalidate our global cache
247             QPixmapCache::clear();
248             d->graphicsLoaded = true;
249             reloadTileset(QSize(d->originaldata.w, d->originaldata.h));
250         } else {
251             return false;
252         }
253     } else {
254         //TODO add support for png??
255         return false;
256     }
257 
258     return true;
259 }
260 
261 // ---------------------------------------------------------
reloadTileset(const QSize & newTilesize)262 bool KMahjonggTileset::reloadTileset(const QSize & newTilesize)
263 {
264     if (QSize(d->scaleddata.w, d->scaleddata.h) == newTilesize) {
265         return false;
266     }
267 
268     if (d->isSVG) {
269         if (d->svg.isValid()) {
270             updateScaleInfo(newTilesize.width(), newTilesize.height());
271             //rendering will be done when needed, automatically using the global cache
272         } else {
273             return false;
274         }
275     } else {
276         //TODO add support for png???
277         return false;
278     }
279 
280     return true;
281 }
282 
buildElementIdTable()283 void KMahjonggTileset::buildElementIdTable()
284 {
285     //Build a list for faster lookup of element ids, mapped to the enumeration used by GameData and BoardWidget
286     //Unselected tiles
287     for (short idx = 1; idx <= 4; idx++) {
288         d->elementIdTable.append(QStringLiteral("TILE_%1").arg(idx));
289     }
290     //Selected tiles
291     for (short idx = 1; idx <= 4; idx++) {
292         d->elementIdTable.append(QStringLiteral("TILE_%1_SEL").arg(idx));
293     }
294     //now faces
295     for (short idx = 1; idx <= 9; idx++) {
296         d->elementIdTable.append(QStringLiteral("CHARACTER_%1").arg(idx));
297     }
298     for (short idx = 1; idx <= 9; idx++) {
299         d->elementIdTable.append(QStringLiteral("BAMBOO_%1").arg(idx));
300     }
301     for (short idx = 1; idx <= 9; idx++) {
302         d->elementIdTable.append(QStringLiteral("ROD_%1").arg(idx));
303     }
304     for (short idx = 1; idx <= 4; idx++) {
305         d->elementIdTable.append(QStringLiteral("SEASON_%1").arg(idx));
306     }
307     for (short idx = 1; idx <= 4; idx++) {
308         d->elementIdTable.append(QStringLiteral("WIND_%1").arg(idx));
309     }
310     for (short idx = 1; idx <= 3; idx++) {
311         d->elementIdTable.append(QStringLiteral("DRAGON_%1").arg(idx));
312     }
313     for (short idx = 1; idx <= 4; idx++) {
314         d->elementIdTable.append(QStringLiteral("FLOWER_%1").arg(idx));
315     }
316 }
317 
pixmapCacheNameFromElementId(const QString & elementid)318 QString KMahjonggTileset::pixmapCacheNameFromElementId(const QString & elementid)
319 {
320     return authorProperty(QStringLiteral("Name")) + elementid + QStringLiteral("W%1H%2").arg(d->scaleddata.w).arg(d->scaleddata.h);
321 }
322 
renderElement(short width,short height,const QString & elementid)323 QPixmap KMahjonggTileset::renderElement(short width, short height, const QString & elementid)
324 {
325     //qCDebug(LIBKMAHJONGG_LOG) << "render element" << elementid << width << height;
326     const qreal dpr = qApp->devicePixelRatio();
327     width = width * dpr;
328     height = height * dpr;
329     QImage qiRend(QSize(width, height), QImage::Format_ARGB32_Premultiplied);
330     qiRend.fill(0);
331 
332     if (d->svg.isValid()) {
333         QPainter p(&qiRend);
334         d->svg.render(&p, elementid);
335     }
336     qiRend.setDevicePixelRatio(dpr);
337     return QPixmap::fromImage(qiRend);
338 }
339 
selectedTile(int num)340 QPixmap KMahjonggTileset::selectedTile(int num)
341 {
342     QPixmap pm;
343     QString elemId = d->elementIdTable.at(num + 4); //selected offset in our idtable;
344     if (!QPixmapCache::find(pixmapCacheNameFromElementId(elemId), &pm)) {
345         //use tile size
346         pm = renderElement(d->scaleddata.w, d->scaleddata.h, elemId);
347         QPixmapCache::insert(pixmapCacheNameFromElementId(elemId), pm);
348     }
349     return pm;
350 }
351 
unselectedTile(int num)352 QPixmap KMahjonggTileset::unselectedTile(int num)
353 {
354     QPixmap pm;
355     QString elemId = d->elementIdTable.at(num);
356     if (!QPixmapCache::find(pixmapCacheNameFromElementId(elemId), &pm)) {
357         //use tile size
358         pm = renderElement(d->scaleddata.w, d->scaleddata.h, elemId);
359         QPixmapCache::insert(pixmapCacheNameFromElementId(elemId), pm);
360     }
361     return pm;
362 }
363 
tileface(int num)364 QPixmap KMahjonggTileset::tileface(int num)
365 {
366     QPixmap pm;
367     if ((num + 8) >= d->elementIdTable.count()) {
368         //qCDebug(LIBKMAHJONGG_LOG) << "Client asked for invalid tileface id";
369         return pm;
370     }
371 
372     QString elemId = d->elementIdTable.at(num + 8); //tileface offset in our idtable;
373     if (!QPixmapCache::find(pixmapCacheNameFromElementId(elemId), &pm)) {
374         //use face size
375         pm = renderElement(d->scaleddata.fw, d->scaleddata.fh, elemId);
376         QPixmapCache::insert(pixmapCacheNameFromElementId(elemId), pm);
377     }
378     return pm;
379 }
380