1 /*
2 * Copyright 2021-2022, Haiku, Inc. All rights reserved.
3 * Distributed under the terms of the MIT License.
4 *
5 * Authors:
6 * Augustin Cavalier <waddlesplash>
7 * John Scipione <jscipione@gmail.com>
8 */
9 #include "Thumbnails.h"
10
11 #include <list>
12 #include <fs_attr.h>
13
14 #include <Application.h>
15 #include <Autolock.h>
16 #include <BitmapStream.h>
17 #include <Mime.h>
18 #include <Node.h>
19 #include <NodeInfo.h>
20 #include <TranslatorFormats.h>
21 #include <TranslatorRoster.h>
22 #include <TranslationUtils.h>
23 #include <TypeConstants.h>
24 #include <View.h>
25 #include <Volume.h>
26
27 #include <AutoDeleter.h>
28 #include <JobQueue.h>
29
30 #include "Attributes.h"
31 #include "Commands.h"
32 #include "FSUtils.h"
33 #include "TrackerSettings.h"
34
35
36 #ifdef B_XXL_ICON
37 # undef B_XXL_ICON
38 #endif
39 #define B_XXL_ICON 128
40
41
42 namespace BPrivate {
43
44
45 // #pragma mark - thumbnail generation
46
47
48 enum ThumbnailWorkers {
49 SMALLER_FILES_WORKER = 0,
50 LARGER_FILES_WORKER,
51
52 TOTAL_THUMBNAIL_WORKERS
53 };
54 using BSupportKit::BPrivate::JobQueue;
55 static JobQueue* sThumbnailWorkers[TOTAL_THUMBNAIL_WORKERS];
56
57 static std::list<GenerateThumbnailJob*> sActiveJobs;
58 static BLocker sActiveJobsLock;
59
60
61 static BRect
ThumbBounds(BBitmap * icon,float aspectRatio)62 ThumbBounds(BBitmap* icon, float aspectRatio)
63 {
64 BRect thumbBounds;
65
66 if ((icon->Bounds().Width() / icon->Bounds().Height()) == aspectRatio)
67 return icon->Bounds();
68
69 if (aspectRatio > 1) {
70 // wide
71 thumbBounds = BRect(0, 0, icon->Bounds().IntegerWidth() - 1,
72 floorf((icon->Bounds().IntegerHeight() - 1) / aspectRatio));
73 thumbBounds.OffsetBySelf(0, floorf((icon->Bounds().IntegerHeight()
74 - thumbBounds.IntegerHeight()) / 2.0f));
75 } else if (aspectRatio < 1) {
76 // tall
77 thumbBounds = BRect(0, 0, floorf((icon->Bounds().IntegerWidth() - 1)
78 * aspectRatio), icon->Bounds().IntegerHeight() - 1);
79 thumbBounds.OffsetBySelf(floorf((icon->Bounds().IntegerWidth()
80 - thumbBounds.IntegerWidth()) / 2.0f), 0);
81 } else {
82 // square
83 thumbBounds = icon->Bounds();
84 }
85
86 return thumbBounds;
87 }
88
89
90 static status_t
ScaleBitmap(BBitmap * source,BBitmap & dest,BRect bounds,color_space colorSpace)91 ScaleBitmap(BBitmap* source, BBitmap& dest, BRect bounds, color_space colorSpace)
92 {
93 dest = BBitmap(bounds, colorSpace, true);
94 BView view(dest.Bounds(), "", B_FOLLOW_NONE, B_WILL_DRAW);
95 dest.AddChild(&view);
96 if (view.LockLooper()) {
97 // fill with transparent
98 view.SetLowColor(B_TRANSPARENT_COLOR);
99 view.FillRect(view.Bounds(), B_SOLID_LOW);
100 // draw bitmap
101 view.SetDrawingMode(B_OP_ALPHA);
102 view.SetBlendingMode(B_PIXEL_ALPHA, B_ALPHA_COMPOSITE);
103 view.DrawBitmap(source, source->Bounds(),
104 ThumbBounds(&dest, source->Bounds().Width()
105 / source->Bounds().Height()),
106 B_FILTER_BITMAP_BILINEAR);
107 view.Sync();
108 view.UnlockLooper();
109 }
110 dest.RemoveChild(&view);
111 return B_OK;
112 }
113
114
115 static status_t
ScaleBitmap(BBitmap * source,BBitmap & dest,BSize size,color_space colorSpace)116 ScaleBitmap(BBitmap* source, BBitmap& dest, BSize size, color_space colorSpace)
117 {
118 return ScaleBitmap(source, dest, BRect(BPoint(0, 0), size), colorSpace);
119 }
120
121
122 class GenerateThumbnailJob : public BSupportKit::BJob {
123 public:
GenerateThumbnailJob(Model * model,const BFile & file,BSize requestedSize,color_space colorSpace)124 GenerateThumbnailJob(Model* model, const BFile& file,
125 BSize requestedSize, color_space colorSpace)
126 : BJob("GenerateThumbnail"),
127 fMimeType(model->MimeType()),
128 fRequestedSize(requestedSize),
129 fColorSpace(colorSpace)
130 {
131 fFile = new(std::nothrow) BFile(file);
132 fFile->GetNodeRef((node_ref*)&fNodeRef);
133
134 BAutolock lock(sActiveJobsLock);
135 sActiveJobs.push_back(this);
136 }
~GenerateThumbnailJob()137 virtual ~GenerateThumbnailJob()
138 {
139 delete fFile;
140
141 BAutolock lock(sActiveJobsLock);
142 sActiveJobs.remove(this);
143 }
144
InitCheck()145 status_t InitCheck()
146 {
147 if (fFile == NULL)
148 return B_NO_MEMORY;
149 return BJob::InitCheck();
150 }
151
152 virtual status_t Execute();
153
154 public:
155 const BString fMimeType;
156 const node_ref fNodeRef;
157 const BSize fRequestedSize;
158 const color_space fColorSpace;
159
160 private:
161 BFile* fFile;
162 };
163
164
165 status_t
Execute()166 GenerateThumbnailJob::Execute()
167 {
168 BBitmapStream imageStream;
169 status_t status = BTranslatorRoster::Default()->Translate(fFile, NULL, NULL,
170 &imageStream, B_TRANSLATOR_BITMAP, 0, fMimeType);
171 if (status != B_OK)
172 return status;
173
174 BBitmap* image;
175 status = imageStream.DetachBitmap(&image);
176 if (status != B_OK)
177 return status;
178
179 // we have translated the image file into a BBitmap
180
181 // now, scale and directly insert into the icon cache
182 BBitmap tmp(NULL, false);
183 ScaleBitmap(image, tmp, fRequestedSize, fColorSpace);
184
185 BBitmap* cacheThumb = new BBitmap(tmp.Bounds(), 0, tmp.ColorSpace());
186 cacheThumb->ImportBits(&tmp);
187
188 NodeIconCache* nodeIconCache = &IconCache::sIconCache->fNodeCache;
189 AutoLocker<NodeIconCache> cacheLocker(nodeIconCache);
190 NodeCacheEntry* entry = nodeIconCache->FindItem(&fNodeRef);
191 if (entry == NULL)
192 entry = nodeIconCache->AddItem(&fNodeRef);
193 if (entry == NULL) {
194 delete cacheThumb;
195 return B_NO_MEMORY;
196 }
197
198 entry->SetIcon(cacheThumb, kNormalIcon, fRequestedSize);
199 cacheLocker.Unlock();
200
201 // write values to attributes
202 bool thumbnailWritten = false;
203 const int32 width = image->Bounds().IntegerWidth() + 1;
204 const size_t written = fFile->WriteAttr("Media:Width", B_INT32_TYPE,
205 0, &width, sizeof(int32));
206 if (written == sizeof(int32)) {
207 // first attribute succeeded, write the rest
208 const int32 height = image->Bounds().IntegerHeight() + 1;
209 fFile->WriteAttr("Media:Height", B_INT32_TYPE, 0, &height, sizeof(int32));
210
211 // convert image into a 128x128 WebP image and stash it
212 BBitmap thumb(NULL, false);
213 ScaleBitmap(image, thumb, B_XXL_ICON, fColorSpace);
214
215 BBitmap* thumbPointer = &thumb;
216 BBitmapStream thumbStream(thumbPointer);
217 BMallocIO stream;
218 if (BTranslatorRoster::Default()->Translate(&thumbStream,
219 NULL, NULL, &stream, B_WEBP_FORMAT) == B_OK
220 && thumbStream.DetachBitmap(&thumbPointer) == B_OK) {
221 // write WebP image data into an attribute
222 status = fFile->WriteAttr(kAttrThumbnail, B_RAW_TYPE, 0,
223 stream.Buffer(), stream.BufferLength());
224 thumbnailWritten = (status == B_OK);
225
226 // write thumbnail creation time into an attribute
227 int64_t created = real_time_clock();
228 fFile->WriteAttr(kAttrThumbnailCreationTime, B_TIME_TYPE,
229 0, &created, sizeof(int64_t));
230 }
231 }
232
233 delete image;
234
235 // Manually trigger an icon refresh, if necessary.
236 // (If the attribute was written, node monitoring will handle this automatically.)
237 if (!thumbnailWritten) {
238 // send Tracker a message to tell it to update the thumbnail
239 BMessage message(kUpdateThumbnail);
240 if (message.AddNodeRef("noderef", &fNodeRef) == B_OK)
241 be_app->PostMessage(&message);
242 }
243
244 return B_OK;
245 }
246
247
248 static status_t
thumbnail_worker(void * castToJobQueue)249 thumbnail_worker(void* castToJobQueue)
250 {
251 JobQueue* queue = (JobQueue*)castToJobQueue;
252 while (true) {
253 BSupportKit::BJob* job;
254 status_t status = queue->Pop(B_INFINITE_TIMEOUT, false, &job);
255 if (status == B_INTERRUPTED)
256 continue;
257 if (status != B_OK)
258 break;
259
260 job->Run();
261 delete job;
262 }
263
264 return B_OK;
265 }
266
267
268 static status_t
GenerateThumbnail(Model * model,color_space colorSpace,BSize size)269 GenerateThumbnail(Model* model, color_space colorSpace, BSize size)
270 {
271 // First check we do not have a job queued already.
272 BAutolock jobsLock(sActiveJobsLock);
273 for (std::list<GenerateThumbnailJob*>::iterator it = sActiveJobs.begin();
274 it != sActiveJobs.end(); it++) {
275 if ((*it)->fNodeRef == *model->NodeRef())
276 return B_BUSY;
277 }
278 jobsLock.Unlock();
279
280 BFile* file = dynamic_cast<BFile*>(model->Node());
281 if (file == NULL)
282 return B_NOT_SUPPORTED;
283
284 struct stat st;
285 status_t status = file->GetStat(&st);
286 if (status != B_OK)
287 return status;
288
289 GenerateThumbnailJob* job = new(std::nothrow) GenerateThumbnailJob(model,
290 *file, size, colorSpace);
291 ObjectDeleter<GenerateThumbnailJob> jobDeleter(job);
292 if (job == NULL)
293 return B_NO_MEMORY;
294 if (job->InitCheck() != B_OK)
295 return job->InitCheck();
296
297 JobQueue** jobQueue;
298 if (st.st_size >= (128 * kKBSize)) {
299 jobQueue = &sThumbnailWorkers[LARGER_FILES_WORKER];
300 } else {
301 jobQueue = &sThumbnailWorkers[SMALLER_FILES_WORKER];
302 }
303
304 if ((*jobQueue) == NULL) {
305 // We need to create the worker.
306 *jobQueue = new(std::nothrow) JobQueue();
307 if ((*jobQueue) == NULL)
308 return B_NO_MEMORY;
309 if ((*jobQueue)->InitCheck() != B_OK)
310 return (*jobQueue)->InitCheck();
311 thread_id thread = spawn_thread(thumbnail_worker, "thumbnail worker",
312 B_NORMAL_PRIORITY, *jobQueue);
313 if (thread < B_OK)
314 return thread;
315 resume_thread(thread);
316 }
317
318 jobDeleter.Detach();
319 status = (*jobQueue)->AddJob(job);
320 if (status == B_OK)
321 return B_BUSY;
322
323 return status;
324 }
325
326
327 // #pragma mark - thumbnail fetching
328
329
330 status_t
GetThumbnailFromAttr(Model * model,BBitmap * icon,BSize size)331 GetThumbnailFromAttr(Model* model, BBitmap* icon, BSize size)
332 {
333 if (model == NULL || icon == NULL)
334 return B_BAD_VALUE;
335
336 status_t result = model->InitCheck();
337 if (result != B_OK)
338 return result;
339
340 result = icon->InitCheck();
341 if (result != B_OK)
342 return result;
343
344 BNode* node = model->Node();
345 if (node == NULL)
346 return B_BAD_VALUE;
347
348 // look for a thumbnail in an attribute
349 time_t modtime;
350 int64_t thumbnailCreated;
351 if (node->GetModificationTime(&modtime) == B_OK
352 && node->ReadAttr(kAttrThumbnailCreationTime, B_TIME_TYPE, 0,
353 &thumbnailCreated, sizeof(int64_t)) == sizeof(int64_t)) {
354 if (thumbnailCreated > modtime) {
355 // file has not changed, try to return an existing thumbnail
356 attr_info attrInfo;
357 if (node->GetAttrInfo(kAttrThumbnail, &attrInfo) == B_OK) {
358 BMallocIO webpData;
359 webpData.SetSize(attrInfo.size);
360 if (node->ReadAttr(kAttrThumbnail, attrInfo.type, 0,
361 (void*)webpData.Buffer(), attrInfo.size) == attrInfo.size) {
362 BBitmap thumb(BTranslationUtils::GetBitmap(&webpData));
363
364 // convert thumb to icon size
365 if ((size.IntegerWidth() + 1) == B_XXL_ICON) {
366 // import icon data from attribute without resizing
367 result = icon->ImportBits(&thumb);
368 } else {
369 // down-scale thumb to icon size
370 // TODO don't make a copy, allow icon to accept views?
371 BBitmap tmp(NULL, false);
372 ScaleBitmap(&thumb, tmp, icon->Bounds(), icon->ColorSpace());
373
374 // copy tmp bitmap into icon
375 result = icon->ImportBits(&tmp);
376 }
377 // we found a thumbnail
378 if (result == B_OK)
379 return result;
380 }
381 }
382 // else we did not find a thumbnail
383 } else {
384 // file changed, remove all thumb attrs
385 char attrName[B_ATTR_NAME_LENGTH];
386 while (node->GetNextAttrName(attrName) == B_OK) {
387 if (BString(attrName).StartsWith(kAttrThumbnail))
388 node->RemoveAttr(attrName);
389 }
390 }
391 }
392
393 if (ShouldGenerateThumbnail(model->MimeType()))
394 return GenerateThumbnail(model, icon->ColorSpace(), size);
395
396 return B_NOT_SUPPORTED;
397 }
398
399
400 bool
ShouldGenerateThumbnail(const char * type)401 ShouldGenerateThumbnail(const char* type)
402 {
403 // check generate thumbnail setting,
404 // mime type must be an image (for now)
405 return TrackerSettings().GenerateImageThumbnails()
406 && type != NULL && BString(type).IStartsWith("image");
407 }
408
409
410 } // namespace BPrivate
411