thumbnail_cache.vala 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250
  1. /*
  2. * Copyright (c) 2012-2024 Daniele Bartolini et al.
  3. * SPDX-License-Identifier: GPL-3.0-or-later
  4. */
  5. namespace Crown
  6. {
  7. public class ThumbnailCache
  8. {
  9. [Compact]
  10. public struct CacheEntry
  11. {
  12. Gdk.Pixbuf? pixbuf; ///< Pixbuf area inside the atlas.
  13. unowned List<StringId64?> lru; ///< Pointer to LRU list entry.
  14. uint64 mtime; ///< Pixbuf last modification time.
  15. bool pending; ///< Whether a request to generate the thumbnail is pending.
  16. }
  17. public const int THUMBNAIL_SIZE = 64;
  18. public Project _project;
  19. public RuntimeInstance _thumbnail;
  20. public Gdk.Pixbuf _atlas;
  21. public GLib.List<StringId64?> _list;
  22. public Gee.HashMap<StringId64?, CacheEntry?> _map;
  23. public uint _max_cache_size;
  24. // Called when the cache changed its content.
  25. public signal void changed();
  26. public ThumbnailCache(Project project, RuntimeInstance thumbnail, uint max_cache_size)
  27. {
  28. _project = project;
  29. _thumbnail = thumbnail;
  30. _list = new GLib.List<StringId64?>();
  31. _map = new Gee.HashMap<StringId64?, CacheEntry?>(StringId64.hash_func, StringId64.equal_func);
  32. reset(max_cache_size);
  33. }
  34. public string thumbnail_path(string resource_path)
  35. {
  36. GLib.File file = GLib.File.new_for_path(_project.absolute_path(resource_path));
  37. uint8 uri_md5[16];
  38. Md5.State st = Md5.State();
  39. st.append(file.get_uri().data);
  40. st.finish(out uri_md5);
  41. string thumb_filename = "%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x.png".printf(uri_md5[0]
  42. , uri_md5[1]
  43. , uri_md5[2]
  44. , uri_md5[3]
  45. , uri_md5[4]
  46. , uri_md5[5]
  47. , uri_md5[6]
  48. , uri_md5[7]
  49. , uri_md5[8]
  50. , uri_md5[9]
  51. , uri_md5[10]
  52. , uri_md5[11]
  53. , uri_md5[12]
  54. , uri_md5[13]
  55. , uri_md5[14]
  56. , uri_md5[15]
  57. );
  58. return GLib.Path.build_filename(_thumbnails_normal_dir.get_path(), thumb_filename);
  59. }
  60. public Gdk.Pixbuf? create_thumbnail_subpixbuf()
  61. {
  62. int thumbs_per_row = _atlas.get_width() / THUMBNAIL_SIZE;
  63. int thumb_row = (int)_list.length() / thumbs_per_row;
  64. int thumb_col = (int)_list.length() % thumbs_per_row;
  65. int dest_x = thumb_col * THUMBNAIL_SIZE;
  66. int dest_y = thumb_row * THUMBNAIL_SIZE;
  67. return new Gdk.Pixbuf.subpixbuf(_atlas
  68. , dest_x
  69. , dest_y
  70. , THUMBNAIL_SIZE
  71. , THUMBNAIL_SIZE
  72. );
  73. }
  74. public void thumbnail_ready(string type, string name, string thumb_path)
  75. {
  76. string resource_path = ResourceId.path(type, name);
  77. StringId64 resource_id = StringId64(resource_path);
  78. // Rename thumb_path to destination atomically.
  79. GLib.File thumb_path_tmp = GLib.File.new_for_path(thumb_path);
  80. GLib.File thumb_path_dst = GLib.File.new_for_path(thumbnail_path(resource_path));
  81. try {
  82. thumb_path_tmp.move(thumb_path_dst, GLib.FileCopyFlags.OVERWRITE);
  83. } catch (GLib.Error e) {
  84. loge(e.message);
  85. }
  86. CacheEntry? entry = _map.get(resource_id);
  87. if (entry == null)
  88. return;
  89. try {
  90. // Read thumbnail from disk.
  91. uint64 thumb_mtime = 0;
  92. GLib.FileInfo thumb_info = thumb_path_dst.query_info("*", GLib.FileQueryInfoFlags.NOFOLLOW_SYMLINKS);
  93. GLib.DateTime? mdate = thumb_info.get_modification_date_time();
  94. if (mdate != null)
  95. thumb_mtime = mdate.to_unix() * 1000000000 + mdate.get_microsecond() * 1000; // Convert to ns.
  96. copy_thumbnail_from_path(entry.pixbuf, thumb_path_dst.get_path());
  97. entry.mtime = thumb_mtime;
  98. entry.pending = false;
  99. _map.set(resource_id, entry);
  100. assert(_map.get(resource_id).mtime == thumb_mtime);
  101. } catch (GLib.Error e) {
  102. loge(e.message);
  103. }
  104. }
  105. // Copies @a thumbnail inside the atlas at the position defined by @a subpixbuf.
  106. public void copy_thumbnail(Gdk.Pixbuf? subpixbuf, Gdk.Pixbuf? thumbnail)
  107. {
  108. thumbnail.copy_area(0
  109. , 0
  110. , THUMBNAIL_SIZE
  111. , THUMBNAIL_SIZE
  112. , subpixbuf
  113. , 0
  114. , 0
  115. );
  116. changed();
  117. }
  118. public void copy_thumbnail_from_path(Gdk.Pixbuf? subpixbuf, string thumbnail_path) throws GLib.Error
  119. {
  120. try {
  121. var thumbnail = new Gdk.Pixbuf.from_file_at_size(thumbnail_path
  122. , THUMBNAIL_SIZE
  123. , THUMBNAIL_SIZE
  124. );
  125. copy_thumbnail(subpixbuf, thumbnail);
  126. } finally {
  127. // Empty.
  128. }
  129. }
  130. public void reset(uint max_size)
  131. {
  132. _list = new GLib.List<StringId64?>(); // No clear?
  133. _map.clear();
  134. int height;
  135. int width = (int)Math.sqrt(max_size);
  136. width /= 4; // 4 bytes per pixel.
  137. width -= width % THUMBNAIL_SIZE;
  138. height = width;
  139. _max_cache_size = (width / THUMBNAIL_SIZE) * (height / THUMBNAIL_SIZE);
  140. _atlas = new Gdk.Pixbuf(Gdk.Colorspace.RGB
  141. , true
  142. , 8
  143. , width
  144. , height
  145. );
  146. }
  147. public Gdk.Pixbuf? get(string type, string name)
  148. {
  149. if (!_project.is_loaded())
  150. return null;
  151. string resource_path = ResourceId.path(type, name);
  152. StringId64 resource_id = StringId64(resource_path);
  153. CacheEntry? entry = null;
  154. // Allocate a subpixbuf slot inside the atlas.
  155. if (_map.has_key(resource_id)) {
  156. entry = _map.get(resource_id);
  157. // Set resource_id as most recently used entry.
  158. _list.remove_link(entry.lru);
  159. _list.append(resource_id);
  160. entry.lru = _list.last();
  161. _map.set(resource_id, entry);
  162. } else {
  163. Gdk.Pixbuf? pixbuf = null;
  164. if (_list.length() == _max_cache_size) {
  165. // Evict the least recently used entry.
  166. unowned List<StringId64?> lru = _list.nth(0);
  167. // Reuse the subpixbuf from the evicted entry.
  168. pixbuf = _map.get(lru.data).pixbuf;
  169. _map.unset(lru.data);
  170. _list.remove_link(lru);
  171. } else {
  172. // Create a new subpixbuf if the entry is new.
  173. pixbuf = create_thumbnail_subpixbuf();
  174. }
  175. uint64 thumb_mtime = 0;
  176. try {
  177. // Read thumbnail from disk.
  178. GLib.File thumb_path_dst = GLib.File.new_for_path(thumbnail_path(resource_path));
  179. GLib.FileInfo thumb_info = thumb_path_dst.query_info("*", GLib.FileQueryInfoFlags.NOFOLLOW_SYMLINKS);
  180. GLib.DateTime? mdate = thumb_info.get_modification_date_time();
  181. if (mdate != null)
  182. thumb_mtime = mdate.to_unix() * 1000000000 + mdate.get_microsecond() * 1000; // Convert to ns.
  183. copy_thumbnail_from_path(pixbuf, thumb_path_dst.get_path());
  184. } catch (GLib.Error e) {
  185. // Nobody cares.
  186. }
  187. // Create a new cache entry.
  188. _list.append(resource_id);
  189. entry = { pixbuf, _list.last(), thumb_mtime, false };
  190. _map.set(resource_id, entry);
  191. }
  192. if (!entry.pending && (entry.mtime == 0 || entry.mtime <= _project.mtime(type, name))) {
  193. // On-disk thumbnail not found or outdated.
  194. // Ask the server to generate a fresh one if the data is ready.
  195. if (_project._data_compiled) {
  196. try {
  197. // Create a unique temporary file to store the thumbnail's data.
  198. FileIOStream fs;
  199. GLib.File thumb_path_tmp = GLib.File.new_tmp(null, out fs);
  200. fs.close();
  201. // Request a new thumbnail.
  202. entry.pending = true;
  203. _map.set(resource_id, entry);
  204. _thumbnail.send_script(ThumbnailApi.add_request(type, name, thumb_path_tmp.get_path()));
  205. _thumbnail.send(DeviceApi.frame());
  206. } catch (GLib.Error e) {
  207. loge(e.message);
  208. }
  209. }
  210. }
  211. return entry.pixbuf;
  212. }
  213. }
  214. } /* namespace Crown */