1// Aseprite
2// Copyright (C) 2019-2022 Igara Studio S.A.
3// Copyright (C) 2001-2018 David Capello
4//
5// This program is distributed under the terms of
6// the End-User License Agreement for Aseprite.
7
8#ifdef HAVE_CONFIG_H
9#include "config.h"
10#endif
11
12#include "app/thumbnail_generator.h"
13
14#include "app/app.h"
15#include "app/cmd/convert_color_profile.h"
16#include "app/doc.h"
17#include "app/file/file.h"
18#include "app/file_system.h"
19#include "app/util/conversion_to_surface.h"
20#include "base/thread.h"
21#include "doc/algorithm/rotate.h"
22#include "doc/image.h"
23#include "doc/palette.h"
24#include "doc/primitives.h"
25#include "doc/sprite.h"
26#include "os/system.h"
27#include "render/projection.h"
28#include "render/render.h"
29#include "ui/system.h"
30
31#include <algorithm>
32#include <atomic>
33#include <memory>
34#include <thread>
35
36#define MAX_THUMBNAIL_SIZE 128
37#define THUMB_TRACE(...)
38
39namespace app {
40
41class ThumbnailGenerator::Worker {
42public:
43 Worker(base::concurrent_queue<ThumbnailGenerator::Item>& queue)
44 : m_queue(queue)
45 , m_fop(nullptr)
46 , m_isDone(false)
47 , m_thread([this]{ loadBgThread(); }) {
48 }
49
50 ~Worker() {
51 {
52 std::lock_guard lock(m_mutex);
53 if (m_fop)
54 m_fop->stop();
55 }
56 m_thread.join();
57 }
58
59 void stop() const {
60 std::lock_guard lock(m_mutex);
61 if (m_fop)
62 m_fop->stop();
63 }
64
65 bool isDone() const {
66 return m_isDone;
67 }
68
69 void updateProgress() {
70 std::lock_guard lock(m_mutex);
71 if (m_item.fileitem && m_item.fop) {
72 double progress = m_item.fop->progress();
73 if (progress > m_item.fileitem->getThumbnailProgress())
74 m_item.fileitem->setThumbnailProgress(progress);
75 }
76 }
77
78private:
79 void loadItem() {
80 ASSERT(!m_fop);
81 try {
82 {
83 std::lock_guard lock(m_mutex);
84 m_fop = m_item.fop;
85 ASSERT(m_fop);
86 }
87
88 THUMB_TRACE("FOP loading thumbnail: %s\n",
89 m_item.fileitem->fileName().c_str());
90
91 // Load the file
92 m_fop->operate(nullptr);
93
94 // Don't call post-load because postLoad() needs user interaction.
95 //m_fop->postLoad();
96
97 // Convert the loaded document into the os::Surface.
98 const Sprite* sprite =
99 (m_fop->document() &&
100 m_fop->document()->sprite() ?
101 m_fop->document()->sprite(): nullptr);
102
103 std::unique_ptr<Image> thumbnailImage;
104 std::unique_ptr<Palette> palette;
105 if (!m_fop->isStop() && sprite) {
106 // The palette to convert the Image
107 palette.reset(new Palette(*sprite->palette(frame_t(0))));
108
109 // Special case for indexed images:
110 // If the sprite is transparent -> set the transparent color index alpha = 0
111 if (sprite->colorMode() == ColorMode::INDEXED &&
112 !sprite->backgroundLayer()) {
113 int i = sprite->transparentColor();
114 if (i >= 0 && i < int(palette->size()))
115 palette->setEntry(i, doc::rgba(0, 0, 0, 0));
116 }
117
118 const int w = sprite->width()*sprite->pixelRatio().w;
119 const int h = sprite->height()*sprite->pixelRatio().h;
120
121 // Calculate the thumbnail size
122 int thumb_w = MAX_THUMBNAIL_SIZE * w / std::max(w, h);
123 int thumb_h = MAX_THUMBNAIL_SIZE * h / std::max(w, h);
124 if (std::max(thumb_w, thumb_h) > std::max(w, h)) {
125 thumb_w = w;
126 thumb_h = h;
127 }
128 thumb_w = std::clamp(thumb_w, 1, MAX_THUMBNAIL_SIZE);
129 thumb_h = std::clamp(thumb_h, 1, MAX_THUMBNAIL_SIZE);
130
131 // Stretch the 'image'
132 thumbnailImage.reset(
133 Image::create(
134 sprite->pixelFormat(), thumb_w, thumb_h));
135
136 render::Projection proj(sprite->pixelRatio(),
137 render::Zoom(thumb_w, w));
138 render::Render render;
139 render.setBgOptions(render::BgOptions::MakeTransparent());
140 render.setProjection(proj);
141 render.renderSprite(
142 thumbnailImage.get(), sprite, frame_t(0),
143 gfx::Clip(0, 0, 0, 0, w, h));
144
145 // Convert the image to sRGB color space
146 auto cs = sprite->colorSpace();
147 if (m_fop->preserveColorProfile() &&
148 cs && !cs->nearlyEqual(*gfx::ColorSpace::MakeSRGB())) {
149 app::cmd::convert_color_profile(
150 thumbnailImage.get(), palette.get(),
151 cs, gfx::ColorSpace::MakeSRGB());
152 }
153 }
154
155 // Close file
156 delete m_fop->releaseDocument();
157
158 // Set the thumbnail of the file-item.
159 if (thumbnailImage) {
160 os::SurfaceRef thumbnail =
161 os::instance()->makeRgbaSurface(
162 thumbnailImage->width(),
163 thumbnailImage->height());
164
165 convert_image_to_surface(
166 thumbnailImage.get(), palette.get(), thumbnail.get(),
167 0, 0, 0, 0, thumbnailImage->width(), thumbnailImage->height());
168
169 {
170 std::lock_guard lock(m_mutex);
171 m_item.fileitem->setThumbnail(thumbnail);
172 }
173 }
174
175 THUMB_TRACE("FOP done with thumbnail: %s %s\n",
176 m_item.fileitem->fileName().c_str(),
177 (m_fop->isStop() ? " (stop)": ""));
178 }
179 catch (const std::exception& e) {
180 m_fop->setError("Error loading file:\n%s", e.what());
181 }
182
183 if (!m_fop->isStop()) {
184 // Set a nullptr thumbnail if we failed loading the given file,
185 // in this way we're not going to re-try generating this same
186 // thumbnail.
187 if (m_item.fileitem->needThumbnail())
188 m_item.fileitem->setThumbnail(nullptr);
189 }
190
191 // Reset the m_item (first the fileitem so this worker is not
192 // associated to this fileitem anymore, and then the FileOp).
193 {
194 std::lock_guard lock(m_mutex);
195 m_item.fileitem = nullptr;
196 }
197
198 m_fop->done();
199 {
200 std::lock_guard lock(m_mutex);
201 m_item.fop = nullptr;
202 delete m_fop;
203 m_fop = nullptr;
204 }
205 ASSERT(!m_fop);
206 }
207
208 void loadBgThread() {
209 while (!m_queue.empty()) {
210 bool success = true;
211 while (success) {
212 {
213 std::lock_guard lock(m_mutex); // To access m_item
214 success = m_queue.try_pop(m_item);
215 }
216 if (success)
217 loadItem();
218 }
219 base::this_thread::yield();
220 }
221 m_isDone = true;
222 }
223
224 base::concurrent_queue<Item>& m_queue;
225 app::ThumbnailGenerator::Item m_item;
226 FileOp* m_fop;
227 mutable std::mutex m_mutex;
228 std::atomic<bool> m_isDone;
229 std::thread m_thread;
230};
231
232ThumbnailGenerator* ThumbnailGenerator::instance()
233{
234 static std::unique_ptr<ThumbnailGenerator> singleton;
235 ui::assert_ui_thread();
236 if (!singleton) {
237 // We cannot use std::make_unique() because ThumbnailGenerator
238 // ctor is private.
239 singleton.reset(new ThumbnailGenerator);
240 App::instance()->Exit.connect([&]{ singleton.reset(); });
241 }
242 return singleton.get();
243}
244
245ThumbnailGenerator::ThumbnailGenerator()
246{
247 int n = std::thread::hardware_concurrency()-1;
248 if (n < 1) n = 1;
249 m_maxWorkers = n;
250}
251
252bool ThumbnailGenerator::checkWorkers()
253{
254 std::lock_guard lock(m_workersAccess);
255 bool doingWork = (!m_workers.empty());
256
257 for (WorkerList::iterator
258 it=m_workers.begin(); it != m_workers.end(); ) {
259 (*it)->updateProgress();
260 if ((*it)->isDone()) {
261 it = m_workers.erase(it);
262 }
263 else {
264 ++it;
265 }
266 }
267
268 return doingWork;
269}
270
271void ThumbnailGenerator::generateThumbnail(IFileItem* fileitem)
272{
273 if (!fileitem->needThumbnail())
274 return;
275
276 if (fileitem->getThumbnailProgress() > 0.0) {
277 if (fileitem->getThumbnailProgress() == 0.00001) {
278 m_remainingItems.prioritize(
279 [fileitem](const Item& item) {
280 return (item.fileitem == fileitem);
281 });
282
283 // If there is no more workers running, we have to start a new
284 // one to process the m_remainingItems queue. How is it possible
285 // that a IFileItem has a thumbnail progress == 0.00001 but
286 // there is no workers? This is an edge case where:
287 // 1. The Worker::loadBgThread() asks for the queue of remaining items
288 // and it's empty, so the thread is going to be closed
289 // 2. We've just created a FOP for this IFileItem and ask for
290 // available workers and we've already launch the max quantity
291 // of possible workers (m_maxWorkers)
292 // 3. All worker threads are just closed so there is no more
293 // worker for the remaining item in the queue.
294 if (m_workers.empty())
295 startWorker();
296 }
297 return;
298 }
299
300 // Set a starting progress so we don't enqueue the same item two times.
301 fileitem->setThumbnailProgress(0.00001);
302
303 THUMB_TRACE("Queue FOP thumbnail for %s\n",
304 fileitem->fileName().c_str());
305
306 std::unique_ptr<FileOp> fop(
307 FileOp::createLoadDocumentOperation(
308 nullptr,
309 fileitem->fileName().c_str(),
310 FILE_LOAD_SEQUENCE_NONE |
311 FILE_LOAD_ONE_FRAME));
312 if (!fop || fop->hasError()) {
313 // Set a nullptr thumbnail so we don't try to generate a thumbnail
314 // for this fileitem again.
315 fileitem->setThumbnail(nullptr);
316 return;
317 }
318
319 m_remainingItems.push(Item(fileitem, fop.get()));
320 fop.release();
321
322 startWorker();
323}
324
325void ThumbnailGenerator::stopAllWorkers()
326{
327 Item item;
328 while (!m_remainingItems.empty()) {
329 while (m_remainingItems.try_pop(item)) {
330 if (!item.fileitem->getThumbnail()) {
331 // Reset progress to 0.0 because the FileOp wasn't used and we
332 // will need to create it again if we require this FileItem
333 // thumbnail again.
334 item.fileitem->setThumbnailProgress(0.0);
335 }
336 delete item.fop;
337 }
338 }
339
340 std::lock_guard lock(m_workersAccess);
341 for (const auto& worker : m_workers)
342 worker->stop();
343}
344
345void ThumbnailGenerator::startWorker()
346{
347 std::lock_guard lock(m_workersAccess);
348 if (m_workers.size() < m_maxWorkers) {
349 m_workers.push_back(std::make_unique<Worker>(m_remainingItems));
350 }
351}
352
353} // namespace app
354