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
42namespace BPrivate {
43
44
45//	#pragma mark - thumbnail generation
46
47
48enum ThumbnailWorkers {
49	SMALLER_FILES_WORKER = 0,
50	LARGER_FILES_WORKER,
51
52	TOTAL_THUMBNAIL_WORKERS
53};
54using BSupportKit::BPrivate::JobQueue;
55static JobQueue* sThumbnailWorkers[TOTAL_THUMBNAIL_WORKERS];
56
57static std::list<GenerateThumbnailJob*> sActiveJobs;
58static BLocker sActiveJobsLock;
59
60
61static BRect
62ThumbBounds(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
90static status_t
91ScaleBitmap(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
115static status_t
116ScaleBitmap(BBitmap* source, BBitmap& dest, BSize size, color_space colorSpace)
117{
118	return ScaleBitmap(source, dest, BRect(BPoint(0, 0), size), colorSpace);
119}
120
121
122class GenerateThumbnailJob : public BSupportKit::BJob {
123public:
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	}
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
154public:
155	const BString fMimeType;
156	const node_ref fNodeRef;
157	const BSize fRequestedSize;
158	const color_space fColorSpace;
159
160private:
161	BFile* fFile;
162};
163
164
165status_t
166GenerateThumbnailJob::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
248static status_t
249thumbnail_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
268static status_t
269GenerateThumbnail(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
330status_t
331GetThumbnailFromAttr(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
400bool
401ShouldGenerateThumbnail(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