1 // Written in the D programming language.
2 
3 /**
4 DLANGUI library.
5 
6 This module contains resource management and drawables implementation.
7 
8 imageCache is RAM cache of decoded images (as DrawBuf).
9 
10 drawableCache is cache of Drawables.
11 
12 Supports nine-patch PNG images in .9.png files (like in Android).
13 
14 Supports state drawables using XML files similar to ones in Android).
15 
16 Synopsis:
17 
18 ----
19 import dlangui.graphics.resources;
20 
21 ----
22 
23 Copyright: Vadim Lopatin, 2014
24 License:   $(WEB boost.org/LICENSE_1_0.txt, Boost License 1.0).
25 Authors:   $(WEB coolreader.org, Vadim Lopatin)
26 */
27 module dlangui.graphics.resources;
28 
29 import dlangui.graphics.images;
30 import dlangui.graphics.drawbuf;
31 import dlangui.core.logger;
32 import std.file;
33 import std.algorithm;
34 import std.xml;
35 import std.algorithm;
36 import std.conv;
37 
38 
39 class Drawable : RefCountedObject {
40 	//private static int _instanceCount;
41 	this() {
42 		//Log.d("Created drawable, count=", ++_instanceCount);
43 	}
44 	~this() {
45 		//Log.d("Destroyed drawable, count=", --_instanceCount);
46 	}
47     abstract void drawTo(DrawBuf buf, Rect rc, uint state = 0, int tilex0 = 0, int tiley0 = 0);
48     @property abstract int width();
49     @property abstract int height();
50     @property Rect padding() { return Rect(0,0,0,0); }
51 }
52 
53 class EmptyDrawable : Drawable {
54     override void drawTo(DrawBuf buf, Rect rc, uint state = 0, int tilex0 = 0, int tiley0 = 0) {
55     }
56     @property override int width() { return 0; }
57     @property override int height() { return 0; }
58 }
59 
60 class SolidFillDrawable : Drawable {
61     protected uint _color;
62     this(uint color) {
63         _color = color;
64     }
65     override void drawTo(DrawBuf buf, Rect rc, uint state = 0, int tilex0 = 0, int tiley0 = 0) {
66         if ((_color >> 24) != 0xFF) // not fully transparent
67             buf.fillRect(rc, _color);
68     }
69     @property override int width() { return 1; }
70     @property override int height() { return 1; }
71 }
72 
73 class ImageDrawable : Drawable {
74     protected DrawBufRef _image;
75     protected bool _tiled;
76 	//private int _instanceCount;
77     this(ref DrawBufRef image, bool tiled = false, bool ninePatch = false) {
78         _image = image;
79         _tiled = tiled;
80         if (ninePatch)
81             _image.detectNinePatch();
82 		//Log.d("Created ImageDrawable, count=", ++_instanceCount);
83     }
84 	~this() {
85 		_image.clear();
86 		//Log.d("Destroyed ImageDrawable, count=", --_instanceCount);
87 	}
88     @property override int width() { 
89         if (_image.isNull)
90             return 0;
91         if (_image.hasNinePatch)
92             return _image.width - 2;
93         return _image.width;
94     }
95     @property override int height() { 
96         if (_image.isNull)
97             return 0;
98         if (_image.hasNinePatch)
99             return _image.height - 2;
100         return _image.height;
101     }
102     @property override Rect padding() { 
103         if (!_image.isNull && _image.hasNinePatch)
104             return _image.ninePatch.padding;
105         return Rect(0,0,0,0); 
106     }
107     private static void correctFrameBounds(ref int n1, ref int n2, ref int n3, ref int n4) {
108         if (n1 > n2) {
109             //assert(n2 - n1 == n4 - n3);
110             int middledist = (n1 + n2) / 2 - n1;
111             n1 = n2 = n1 + middledist;
112             n3 = n4 = n3 + middledist;
113         }
114     }
115     override void drawTo(DrawBuf buf, Rect rc, uint state = 0, int tilex0 = 0, int tiley0 = 0) {
116         if (_image.isNull)
117             return;
118         if (_image.hasNinePatch) {
119             // draw nine patch
120             const NinePatch * p = _image.ninePatch;
121             //Log.d("drawing nine patch image with frame ", p.frame, " padding ", p.padding);
122             int w = width;
123             int h = height;
124             Rect dstrect = rc;
125             Rect srcrect = Rect(1, 1, w + 1, h + 1);
126             if (true) { //buf.applyClipping(dstrect, srcrect)) {
127                 int x0 = srcrect.left;
128                 int x1 = srcrect.left + p.frame.left;
129                 int x2 = srcrect.right - p.frame.right;
130                 int x3 = srcrect.right;
131                 int y0 = srcrect.top;
132                 int y1 = srcrect.top + p.frame.top;
133                 int y2 = srcrect.bottom - p.frame.bottom;
134                 int y3 = srcrect.bottom;
135                 int dstx0 = rc.left;
136                 int dstx1 = rc.left + p.frame.left;
137                 int dstx2 = rc.right - p.frame.right;
138                 int dstx3 = rc.right;
139                 int dsty0 = rc.top;
140                 int dsty1 = rc.top + p.frame.top;
141                 int dsty2 = rc.bottom - p.frame.bottom;
142                 int dsty3 = rc.bottom;
143                 //Log.d("x bounds: ", x0, ", ", x1, ", ", x2, ", ", x3, " dst ", dstx0, ", ", dstx1, ", ", dstx2, ", ", dstx3);
144                 //Log.d("y bounds: ", y0, ", ", y1, ", ", y2, ", ", y3, " dst ", dsty0, ", ", dsty1, ", ", dsty2, ", ", dsty3);
145 
146                 correctFrameBounds(x1, x2, dstx1, dstx2);
147                 correctFrameBounds(y1, y2, dsty1, dsty2);
148 
149                 //correctFrameBounds(x1, x2);
150                 //correctFrameBounds(y1, y2);
151                 //correctFrameBounds(dstx1, dstx2);
152                 //correctFrameBounds(dsty1, dsty2);
153                 if (y0 < y1 && dsty0 < dsty1) {
154                     // top row
155                     if (x0 < x1 && dstx0 < dstx1)
156                         buf.drawFragment(dstx0, dsty0, _image.get, Rect(x0, y0, x1, y1)); // top left
157                     if (x1 < x2 && dstx1 < dstx2)
158                         buf.drawRescaled(Rect(dstx1, dsty0, dstx2, dsty1), _image.get, Rect(x1, y0, x2, y1)); // top center
159                     if (x2 < x3 && dstx2 < dstx3)
160                         buf.drawFragment(dstx2, dsty0, _image.get, Rect(x2, y0, x3, y1)); // top right
161                 }
162                 if (y1 < y2 && dsty1 < dsty2) {
163                     // middle row
164                     if (x0 < x1 && dstx0 < dstx1)
165                         buf.drawRescaled(Rect(dstx0, dsty1, dstx1, dsty2), _image.get, Rect(x0, y1, x1, y2)); // middle center
166                     if (x1 < x2 && dstx1 < dstx2)
167                         buf.drawRescaled(Rect(dstx1, dsty1, dstx2, dsty2), _image.get, Rect(x1, y1, x2, y2)); // center
168                     if (x2 < x3 && dstx2 < dstx3)
169                         buf.drawRescaled(Rect(dstx2, dsty1, dstx3, dsty2), _image.get, Rect(x2, y1, x3, y2)); // middle center
170                 }
171                 if (y2 < y3 && dsty2 < dsty3) {
172                     // bottom row
173                     if (x0 < x1 && dstx0 < dstx1)
174                         buf.drawFragment(dstx0, dsty2, _image.get, Rect(x0, y2, x1, y3)); // bottom left
175                     if (x1 < x2 && dstx1 < dstx2)
176                         buf.drawRescaled(Rect(dstx1, dsty2, dstx2, dsty3), _image.get, Rect(x1, y2, x2, y3)); // bottom center
177                     if (x2 < x3 && dstx2 < dstx3)
178                         buf.drawFragment(dstx2, dsty2, _image.get, Rect(x2, y2, x3, y3)); // bottom right
179                 }
180             }
181         } else if (_tiled) {
182             // tiled
183         } else {
184             // rescaled or normal
185             if (rc.width != _image.width || rc.height != _image.height)
186                 buf.drawRescaled(rc, _image.get, Rect(0, 0, _image.width, _image.height));
187             else
188                 buf.drawImage(rc.left, rc.top, _image);
189         }
190     }
191 }
192 
193 string attrValue(Element item, string attrname, string attrname2) {
194     if (attrname in item.tag.attr)
195         return item.tag.attr[attrname];
196     if (attrname2 in item.tag.attr)
197         return item.tag.attr[attrname2];
198     return null;
199 }
200 
201 string attrValue(ref string[string] attr, string attrname, string attrname2) {
202     if (attrname in attr)
203         return attr[attrname];
204     if (attrname2 in attr)
205         return attr[attrname2];
206     return null;
207 }
208 
209 void extractStateFlag(ref string[string] attr, string attrName, string attrName2, State state, ref uint stateMask, ref uint stateValue) {
210     string value = attrValue(attr, attrName, attrName2);
211     if (value !is null) {
212         if (value.equal("true"))
213             stateValue |= state;
214         stateMask |= state;
215     }
216 }
217 
218 /// converts XML attribute name to State (see http://developer.android.com/guide/topics/resources/drawable-resource.html#StateList)
219 void extractStateFlags(ref string[string] attr, ref uint stateMask, ref uint stateValue) {
220     extractStateFlag(attr, "state_pressed", "android:state_pressed", State.Pressed, stateMask, stateValue);
221     extractStateFlag(attr, "state_focused", "android:state_focused", State.Focused, stateMask, stateValue);
222     extractStateFlag(attr, "state_hovered", "android:state_hovered", State.Hovered, stateMask, stateValue);
223     extractStateFlag(attr, "state_selected", "android:state_selected", State.Selected, stateMask, stateValue);
224     extractStateFlag(attr, "state_checkable", "android:state_checkable", State.Checkable, stateMask, stateValue);
225     extractStateFlag(attr, "state_checked", "android:state_checked", State.Checked, stateMask, stateValue);
226     extractStateFlag(attr, "state_enabled", "android:state_enabled", State.Enabled, stateMask, stateValue);
227     extractStateFlag(attr, "state_activated", "android:state_activated", State.Activated, stateMask, stateValue);
228     extractStateFlag(attr, "state_window_focused", "android:state_window_focused", State.WindowFocused, stateMask, stateValue);
229 }
230 
231 /*
232 sample:
233 (prefix android: is optional)
234 
235 <?xml version="1.0" encoding="utf-8"?>
236 <selector xmlns:android="http://schemas.android.com/apk/res/android"
237 android:constantSize=["true" | "false"]
238 android:dither=["true" | "false"]
239 android:variablePadding=["true" | "false"] >
240 <item
241 android:drawable="@[package:]drawable/drawable_resource"
242 android:state_pressed=["true" | "false"]
243 android:state_focused=["true" | "false"]
244 android:state_hovered=["true" | "false"]
245 android:state_selected=["true" | "false"]
246 android:state_checkable=["true" | "false"]
247 android:state_checked=["true" | "false"]
248 android:state_enabled=["true" | "false"]
249 android:state_activated=["true" | "false"]
250 android:state_window_focused=["true" | "false"] />
251 </selector>
252 */
253 
254 /// Drawable which is drawn depending on state (see http://developer.android.com/guide/topics/resources/drawable-resource.html#StateList)
255 class StateDrawable : Drawable {
256 
257     static struct StateItem {
258         uint stateMask;
259         uint stateValue;
260         ColorTransform transform;
261         DrawableRef drawable;
262         @property bool matchState(uint state) {
263             return (stateMask & state) == stateValue;
264         }
265     }
266     // list of states
267     protected StateItem[] _stateList;
268     // max paddings for all states
269     protected Rect _paddings;
270     // max drawable size for all states
271     protected Point _size;
272 
273     void addState(uint stateMask, uint stateValue, string resourceId, ref ColorTransform transform) {
274         StateItem item;
275         item.stateMask = stateMask;
276         item.stateValue = stateValue;
277         item.drawable = drawableCache.get(resourceId, transform);
278         itemAdded(item);
279     }
280 
281     void addState(uint stateMask, uint stateValue, DrawableRef drawable) {
282         StateItem item;
283         item.stateMask = stateMask;
284         item.stateValue = stateValue;
285         item.drawable = drawable;
286         itemAdded(item);
287     }
288 
289     private void itemAdded(ref StateItem item) {
290         _stateList ~= item;
291         if (!item.drawable.isNull) {
292             if (_size.x < item.drawable.width)
293                 _size.x = item.drawable.width;
294             if (_size.y < item.drawable.height)
295                 _size.y = item.drawable.height;
296             _paddings.setMax(item.drawable.padding);
297         }
298     }
299 
300     /// parse 4 comma delimited integers
301     static bool parseList4(T)(string value, ref T[4] items) {
302         int index = 0;
303         int p = 0;
304         int start = 0;
305         for (;p < value.length && index < 4; p++) {
306             while (p < value.length && value[p] != ',')
307                 p++;
308             if (p > start) {
309                 int end = p;
310                 string s = value[start .. end];
311                 items[index++] = to!T(s);
312                 start = p + 1;
313             }
314         }
315         return index == 4;
316     }
317     private static uint colorTransformFromStringAdd(string value) {
318         if (value is null)
319             return COLOR_TRANSFORM_OFFSET_NONE;
320         int n[4];
321         if (!parseList4(value, n))
322             return COLOR_TRANSFORM_OFFSET_NONE;
323         foreach (ref item; n) {
324             item = item / 2 + 0x80;
325             if (item < 0)
326                 item = 0;
327             if (item > 0xFF)
328                 item = 0xFF;
329         }
330         return (n[0] << 24) | (n[1] << 16) | (n[2] << 8) | (n[3] << 0);
331     }
332     private static uint colorTransformFromStringMult(string value) {
333         if (value is null)
334             return COLOR_TRANSFORM_MULTIPLY_NONE;
335         float n[4];
336         uint nn[4];
337         if (!parseList4!float(value, n))
338             return COLOR_TRANSFORM_MULTIPLY_NONE;
339         for(int i = 0; i < 4; i++) {
340             int res = cast(int)(n[i] * 0x40);
341             if (res < 0)
342                 res = 0;
343             if (res > 0xFF)
344                 res = 0xFF;
345             nn[i] = res;
346         }
347         return (nn[0] << 24) | (nn[1] << 16) | (nn[2] << 8) | (nn[3] << 0);
348     }
349 
350     bool load(Element element) {
351         foreach(item; element.elements) {
352             if (item.tag.name.equal("item")) {
353                 string drawableId = attrValue(item, "drawable", "android:drawable");
354                 if (drawableId.startsWith("@drawable/"))
355                     drawableId = drawableId[10 .. $];
356                 ColorTransform transform;
357                 transform.addBefore = colorTransformFromStringAdd(attrValue(item, "color_transform_add1", "android:transform_color_add1"));
358                 transform.multiply = colorTransformFromStringMult(attrValue(item, "color_transform_mul", "android:transform_color_mul"));
359                 transform.addAfter = colorTransformFromStringAdd(attrValue(item, "color_transform_add2", "android:transform_color_add2"));
360                 if (drawableId !is null) {
361                     uint stateMask, stateValue;
362                     extractStateFlags(item.tag.attr, stateMask, stateValue);
363                     if (drawableId !is null) {
364                         addState(stateMask, stateValue, drawableId, transform);
365                     }
366                 }
367             }
368         }
369         return _stateList.length > 0;
370     }
371 
372     /// load from XML file
373     bool load(string filename) {
374         import std.file;
375         import std.string;
376 
377         try {
378             string s = cast(string)std.file.read(filename);
379 
380             // Check for well-formedness
381             //check(s);
382 
383             // Make a DOM tree
384             auto doc = new Document(s);
385 
386             return load(doc);
387         } catch (CheckException e) {
388             Log.e("Invalid XML file ", filename);
389             return false;
390         } catch (Throwable e) {
391             Log.e("Cannot read drawable resource from file ", filename);
392             return false;
393         }
394     }
395 
396     override void drawTo(DrawBuf buf, Rect rc, uint state = 0, int tilex0 = 0, int tiley0 = 0) {
397         foreach(ref item; _stateList)
398             if (item.matchState(state)) {
399                 if (!item.drawable.isNull) {
400                     if (state & State.Checked) {
401                         Log.d("Found item for checked state: ", item.stateMask, " ", item.stateValue);
402                     }
403                     item.drawable.drawTo(buf, rc, state, tilex0, tiley0);
404                 }
405                 return;
406             }
407     }
408 
409     @property override int width() {
410         return _size.x;
411     }
412     @property override int height() {
413         return _size.y;
414     }
415     @property override Rect padding() { 
416         return _paddings;
417     }
418 }
419 
420 alias DrawableRef = Ref!Drawable;
421 
422 
423 
424 
425 
426 /// decoded image cache
427 class ImageCache {
428 
429     static class ImageCacheItem {
430         string _filename;
431         DrawBufRef _drawbuf;
432         DrawBufRef[ColorTransform] _transformMap;
433 
434         bool _error; // flag to avoid loading of file if it has been failed once
435         bool _used;
436         this(string filename) {
437             _filename = filename;
438         }
439         /// get normal image
440         @property ref DrawBufRef get() {
441             if (!_drawbuf.isNull || _error) {
442                 _used = true;
443                 return _drawbuf;
444             }
445             _drawbuf = loadImage(_filename);
446             if (_filename.endsWith(".9.png"))
447                 _drawbuf.detectNinePatch();
448             _used = true;
449             if (_drawbuf.isNull)
450                 _error = true;
451             return _drawbuf;
452         }
453         /// get color transformed image
454         @property ref DrawBufRef get(ref ColorTransform transform) {
455             if (transform.empty)
456                 return get();
457             if (transform in _transformMap)
458                 return _transformMap[transform];
459             DrawBufRef src = get();
460             if (src.isNull)
461                 _transformMap[transform] = src;
462             else {            
463                 DrawBufRef t = src.transformColors(transform);
464                 _transformMap[transform] = t;
465             }
466             return _transformMap[transform];
467         }
468         /// remove from memory, will cause reload on next access
469         void compact() {
470             if (!_drawbuf.isNull)
471                 _drawbuf.clear();
472         }
473         /// mark as not used
474         void checkpoint() {
475             _used = false;
476         }
477         /// cleanup if unused since last checkpoint
478         void cleanup() {
479             if (!_used)
480                 compact();
481         }
482     }
483     ImageCacheItem[string] _map;
484 
485     /// get and cache image
486     ref DrawBufRef get(string filename) {
487         if (filename in _map) {
488             return _map[filename].get;
489         }
490         ImageCacheItem item = new ImageCacheItem(filename);
491         _map[filename] = item;
492         return item.get;
493     }
494     /// get and cache color transformed image
495     ref DrawBufRef get(string filename, ref ColorTransform transform) {
496         if (transform.empty)
497             return get(filename);
498         if (filename in _map) {
499             return _map[filename].get(transform);
500         }
501         ImageCacheItem item = new ImageCacheItem(filename);
502         _map[filename] = item;
503         return item.get(transform);
504     }
505 	// clear usage flags for all entries
506 	void checkpoint() {
507         foreach (item; _map)
508             item.checkpoint();
509     }
510 	// removes entries not used after last call of checkpoint() or cleanup()
511 	void cleanup() {
512         foreach (item; _map)
513             item.cleanup();
514     }
515 
516     this() {
517         Log.i("Creating ImageCache");
518     }
519     ~this() {
520         Log.i("Destroying ImageCache");
521 		foreach (ref item; _map) {
522 			destroy(item);
523             item = null;
524 		}
525 		_map.clear();
526     }
527 }
528 
529 __gshared ImageCache _imageCache;
530 /// image cache singleton
531 @property ImageCache imageCache() { return _imageCache; }
532 /// image cache singleton
533 @property void imageCache(ImageCache cache) { 
534 	if (_imageCache !is null)
535 		destroy(_imageCache);
536 	_imageCache = cache; 
537 }
538 
539 __gshared DrawableCache _drawableCache;
540 /// drawable cache singleton
541 @property DrawableCache drawableCache() { return _drawableCache; }
542 /// drawable cache singleton
543 @property void drawableCache(DrawableCache cache) { 
544 	if (_drawableCache !is null)
545 		destroy(_drawableCache);
546 	_drawableCache = cache;
547 }
548 
549 shared static this() {
550     _imageCache = new ImageCache();
551     _drawableCache = new DrawableCache();
552 }
553 
554 class DrawableCache {
555     static class DrawableCacheItem {
556         string _id;
557         string _filename;
558         bool _tiled;
559         bool _error;
560         bool _used;
561         DrawableRef _drawable;
562         DrawableRef[ColorTransform] _transformed;
563 
564 		//private int _instanceCount;
565         this(string id, string filename, bool tiled) {
566             _id = id;
567             _filename = filename;
568             _tiled = tiled;
569             _error = filename is null;
570 			//Log.d("Created DrawableCacheItem, count=", ++_instanceCount);
571         }
572 		~this() {
573 			_drawable.clear();
574 			//Log.d("Destroyed DrawableCacheItem, count=", --_instanceCount);
575 		}
576         /// remove from memory, will cause reload on next access
577         void compact() {
578             if (!_drawable.isNull)
579                 _drawable.clear();
580         }
581         /// mark as not used
582         void checkpoint() {
583             _used = false;
584         }
585         /// cleanup if unused since last checkpoint
586         void cleanup() {
587             if (!_used)
588                 compact();
589         }
590         /// returns drawable (loads from file if necessary)
591         @property ref DrawableRef drawable() {
592             _used = true;
593             if (!_drawable.isNull || _error)
594                 return _drawable;
595             if (_filename !is null) {
596                 // reload from file
597                 if (_filename.endsWith(".xml")) {
598                     // XML drawables support
599                     StateDrawable d = new StateDrawable();
600                     if (!d.load(_filename)) {
601                         destroy(d);
602                         _error = true;
603                     } else {
604                         _drawable = d;
605                     }
606                 } else {
607                     // PNG/JPEG drawables support
608                     DrawBufRef image = imageCache.get(_filename);
609                     if (!image.isNull) {
610                         bool ninePatch = _filename.endsWith(".9.png");
611                         _drawable = new ImageDrawable(image, _tiled, ninePatch);
612                     } else
613                         _error = true;
614                 }
615             }
616             return _drawable;
617         }
618         /// returns drawable (loads from file if necessary)
619         @property ref DrawableRef drawable(ref ColorTransform transform) {
620             if (transform.empty)
621                 return drawable();
622             if (transform in _transformed)
623                 return _transformed[transform];
624             _used = true;
625             if (!_drawable.isNull || _error)
626                 return _drawable;
627             if (_filename !is null) {
628                 // reload from file
629                 if (_filename.endsWith(".xml") || _filename.endsWith(".XML")) {
630                     // XML drawables support
631                     StateDrawable d = new StateDrawable();
632                     if (!d.load(_filename)) {
633                         Log.e("failed to load .xml drawable from ", _filename);
634                         destroy(d);
635                         _error = true;
636                     } else {
637                         Log.d("loaded .xml drawable from ", _filename);
638                         _drawable = d;
639                     }
640                 } else {
641                     // PNG/JPEG drawables support
642                     DrawBufRef image = imageCache.get(_filename, transform);
643                     if (!image.isNull) {
644                         bool ninePatch = _filename.endsWith(".9.png") ||  _filename.endsWith(".9.PNG");
645                         _transformed[transform] = new ImageDrawable(image, _tiled, ninePatch);
646                         return _transformed[transform];
647                     } else {
648                         Log.e("failed to load image from ", _filename);
649                         _error = true;
650                     }
651                 }
652             }
653             return _drawable;
654         }
655     }
656     void clear() {
657 		Log.d("DrawableCache.clear()");
658         _idToFileMap.clear();
659         foreach(DrawableCacheItem item; _idToDrawableMap)
660             item.drawable.clear();
661         _idToDrawableMap.clear();
662     }
663 	// clear usage flags for all entries
664 	void checkpoint() {
665         foreach (item; _idToDrawableMap)
666             item.checkpoint();
667     }
668 	// removes entries not used after last call of checkpoint() or cleanup()
669 	void cleanup() {
670         foreach (item; _idToDrawableMap)
671             item.cleanup();
672     }
673     string[] _resourcePaths;
674     string[string] _idToFileMap;
675     DrawableCacheItem[string] _idToDrawableMap;
676     DrawableRef _nullDrawable;
677     ref DrawableRef get(string id) {
678         if (id.equal("@null"))
679             return _nullDrawable;
680         if (id in _idToDrawableMap)
681             return _idToDrawableMap[id].drawable;
682         string resourceId = id;
683         bool tiled = false;
684         if (id.endsWith(".tiled")) {
685             resourceId = id[0..$-6]; // remove .tiled
686             tiled = true;
687         }
688         string filename = findResource(resourceId);
689         DrawableCacheItem item = new DrawableCacheItem(id, filename, tiled);
690         _idToDrawableMap[id] = item;
691         return item.drawable;
692     }
693     ref DrawableRef get(string id, ref ColorTransform transform) {
694         if (transform.empty)
695             return get(id);
696         if (id.equal("@null"))
697             return _nullDrawable;
698         if (id in _idToDrawableMap)
699             return _idToDrawableMap[id].drawable(transform);
700         string resourceId = id;
701         bool tiled = false;
702         if (id.endsWith(".tiled")) {
703             resourceId = id[0..$-6]; // remove .tiled
704             tiled = true;
705         }
706         string filename = findResource(resourceId);
707         DrawableCacheItem item = new DrawableCacheItem(id, filename, tiled);
708         _idToDrawableMap[id] = item;
709         return item.drawable(transform);
710     }
711     @property string[] resourcePaths() {
712         return _resourcePaths;
713     }
714     /// set resource directory paths as variable number of parameters
715     void setResourcePaths(string[] paths ...) {
716         resourcePaths(paths);
717     }
718     /// set resource directory paths array (only existing dirs will be added)
719     @property void resourcePaths(string[] paths) {
720         string[] existingPaths;
721         foreach(path; paths) {
722             if (exists(path) && isDir(path)) {
723                 existingPaths ~= path;
724                 Log.d("DrawableCache: adding path ", path, " to resource dir list.");
725             } else {
726                 Log.d("DrawableCache: path ", path, " does not exist.");
727             }
728         }
729         _resourcePaths = existingPaths;
730         clear();
731     }
732     /// concatenates path with resource id and extension, returns pathname if there is such file, null if file does not exist
733     private string checkFileName(string path, string id, string extension) {
734         char[] fn = path.dup;
735         fn ~= id;
736         fn ~= extension;
737         if (exists(fn) && isFile(fn))
738             return fn.dup;
739         return null;
740     }
741     string findResource(string id) {
742         if (id in _idToFileMap)
743             return _idToFileMap[id];
744         foreach(string path; _resourcePaths) {
745             string fn;
746             fn = checkFileName(path, id, ".xml");
747             if (fn is null)
748                 fn = checkFileName(path, id, ".png");
749             if (fn is null)
750                 fn = checkFileName(path, id, ".9.png");
751             if (fn is null)
752                 fn = checkFileName(path, id, ".jpg");
753             if (fn !is null) {
754                 _idToFileMap[id] = fn;
755                 return fn;
756             } else {
757                 Log.w("resource ", id, " is not found");
758             }
759         }
760         return null;
761     }
762     this() {
763         Log.i("Creating DrawableCache");
764     }
765     ~this() {
766         Log.i("Destroying DrawableCache");
767 		foreach (ref item; _idToDrawableMap) {
768 			destroy(item);
769 			item = null;
770 		}
771 		_idToDrawableMap.clear();
772     }
773 }
774