xref: /haiku/src/kits/tracker/Thumbnails.cpp (revision 0604d554e86e2efd500ad49d12388258bb994dbe)
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
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
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
116 ScaleBitmap(BBitmap* source, BBitmap& dest, icon_size size, color_space colorSpace)
117 {
118 	return ScaleBitmap(source, dest, BRect(0, 0, size - 1, size - 1), colorSpace);
119 }
120 
121 
122 class GenerateThumbnailJob : public BSupportKit::BJob {
123 public:
124 	GenerateThumbnailJob(Model* model, const BFile& file,
125 			icon_size size, color_space colorSpace)
126 		: BJob("GenerateThumbnail"),
127 		  fMimeType(model->MimeType()),
128 		  fSize(size),
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 	}
137 	virtual ~GenerateThumbnailJob()
138 	{
139 		delete fFile;
140 
141 		BAutolock lock(sActiveJobsLock);
142 		sActiveJobs.remove(this);
143 	}
144 
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 icon_size fSize;
158 	const color_space fColorSpace;
159 
160 private:
161 	BFile* fFile;
162 };
163 
164 
165 status_t
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, fSize, 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, fSize);
199 	cacheLocker.Unlock();
200 
201 	// write values to attributes
202 	bool thumbnailWritten = false;
203 	const int32 width = image->Bounds().IntegerWidth();
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();
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 			bigtime_t created = system_time();
228 			fFile->WriteAttr(kAttrThumbnailCreationTime, B_TIME_TYPE,
229 				0, &created, sizeof(bigtime_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
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
269 GenerateThumbnail(Model* model, color_space colorSpace, icon_size which)
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, which, 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
331 GetThumbnailFromAttr(Model* model, BBitmap* icon, icon_size which)
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 	bigtime_t created;
351 	if (node->GetModificationTime(&modtime) == B_OK
352 		&& node->ReadAttr(kAttrThumbnailCreationTime, B_TIME_TYPE, 0,
353 			&created, sizeof(bigtime_t)) == sizeof(bigtime_t)) {
354 		if (created > (bigtime_t)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 (which == 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(), which);
395 
396 	return B_NOT_SUPPORTED;
397 }
398 
399 
400 bool
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