⛏️ index : haiku.git

/*
 * Copyright 2021-2022, Haiku, Inc. All rights reserved.
 * Distributed under the terms of the MIT License.
 *
 * Authors:
 *		Augustin Cavalier <waddlesplash>
 *		John Scipione <jscipione@gmail.com>
 */
#include "Thumbnails.h"

#include <list>
#include <fs_attr.h>

#include <Application.h>
#include <Autolock.h>
#include <BitmapStream.h>
#include <Mime.h>
#include <Node.h>
#include <NodeInfo.h>
#include <TranslatorFormats.h>
#include <TranslatorRoster.h>
#include <TranslationUtils.h>
#include <TypeConstants.h>
#include <View.h>
#include <Volume.h>

#include <AutoDeleter.h>
#include <JobQueue.h>

#include "Attributes.h"
#include "Commands.h"
#include "FSUtils.h"
#include "TrackerSettings.h"


#ifdef B_XXL_ICON
#	undef B_XXL_ICON
#endif
#define B_XXL_ICON 128


namespace BPrivate {


//	#pragma mark - thumbnail generation


enum ThumbnailWorkers {
	SMALLER_FILES_WORKER = 0,
	LARGER_FILES_WORKER,

	TOTAL_THUMBNAIL_WORKERS
};
using BSupportKit::BPrivate::JobQueue;
static JobQueue* sThumbnailWorkers[TOTAL_THUMBNAIL_WORKERS];

static std::list<GenerateThumbnailJob*> sActiveJobs;
static BLocker sActiveJobsLock;


static BRect
ThumbBounds(BBitmap* icon, float aspectRatio)
{
	BRect thumbBounds;

	if ((icon->Bounds().Width() / icon->Bounds().Height()) == aspectRatio)
		return icon->Bounds();

	if (aspectRatio > 1) {
		// wide
		thumbBounds = BRect(0, 0, icon->Bounds().IntegerWidth() - 1,
			floorf((icon->Bounds().IntegerHeight() - 1) / aspectRatio));
		thumbBounds.OffsetBySelf(0, floorf((icon->Bounds().IntegerHeight()
			- thumbBounds.IntegerHeight()) / 2.0f));
	} else if (aspectRatio < 1) {
		// tall
		thumbBounds = BRect(0, 0, floorf((icon->Bounds().IntegerWidth() - 1)
			* aspectRatio), icon->Bounds().IntegerHeight() - 1);
		thumbBounds.OffsetBySelf(floorf((icon->Bounds().IntegerWidth()
			- thumbBounds.IntegerWidth()) / 2.0f), 0);
	} else {
		// square
		thumbBounds = icon->Bounds();
	}

	return thumbBounds;
}


static status_t
ScaleBitmap(BBitmap* source, BBitmap& dest, BRect bounds, color_space colorSpace)
{
	dest = BBitmap(bounds, colorSpace, true);
	BView view(dest.Bounds(), "", B_FOLLOW_NONE, B_WILL_DRAW);
	dest.AddChild(&view);
	if (view.LockLooper()) {
		// fill with transparent
		view.SetLowColor(B_TRANSPARENT_COLOR);
		view.FillRect(view.Bounds(), B_SOLID_LOW);
		// draw bitmap
		view.SetDrawingMode(B_OP_ALPHA);
		view.SetBlendingMode(B_PIXEL_ALPHA, B_ALPHA_COMPOSITE);
		view.DrawBitmap(source, source->Bounds(),
			ThumbBounds(&dest, source->Bounds().Width()
				/ source->Bounds().Height()),
			B_FILTER_BITMAP_BILINEAR);
		view.Sync();
		view.UnlockLooper();
	}
	dest.RemoveChild(&view);
	return B_OK;
}


static status_t
ScaleBitmap(BBitmap* source, BBitmap& dest, BSize size, color_space colorSpace)
{
	return ScaleBitmap(source, dest, BRect(BPoint(0, 0), size), colorSpace);
}


class GenerateThumbnailJob : public BSupportKit::BJob {
public:
	GenerateThumbnailJob(Model* model, const BFile& file,
			BSize requestedSize, color_space colorSpace)
		: BJob("GenerateThumbnail"),
		  fMimeType(model->MimeType()),
		  fRequestedSize(requestedSize),
		  fColorSpace(colorSpace)
	{
		fFile = new(std::nothrow) BFile(file);
		fFile->GetNodeRef((node_ref*)&fNodeRef);

		BAutolock lock(sActiveJobsLock);
		sActiveJobs.push_back(this);
	}
	virtual ~GenerateThumbnailJob()
	{
		delete fFile;

		BAutolock lock(sActiveJobsLock);
		sActiveJobs.remove(this);
	}

	status_t InitCheck()
	{
		if (fFile == NULL)
			return B_NO_MEMORY;
		return BJob::InitCheck();
	}

	virtual status_t Execute();

public:
	const BString fMimeType;
	const node_ref fNodeRef;
	const BSize fRequestedSize;
	const color_space fColorSpace;

private:
	BFile* fFile;
};


status_t
GenerateThumbnailJob::Execute()
{
	BBitmapStream imageStream;
	status_t status = BTranslatorRoster::Default()->Translate(fFile, NULL, NULL,
		&imageStream, B_TRANSLATOR_BITMAP, 0, fMimeType);
	if (status != B_OK)
		return status;

	BBitmap* image;
	status = imageStream.DetachBitmap(&image);
	if (status != B_OK)
		return status;

	// we have translated the image file into a BBitmap

	// now, scale and directly insert into the icon cache
	BBitmap tmp(NULL, false);
	ScaleBitmap(image, tmp, fRequestedSize, fColorSpace);

	BBitmap* cacheThumb = new BBitmap(tmp.Bounds(), 0, tmp.ColorSpace());
	cacheThumb->ImportBits(&tmp);

	NodeIconCache* nodeIconCache = &IconCache::sIconCache->fNodeCache;
	AutoLocker<NodeIconCache> cacheLocker(nodeIconCache);
	NodeCacheEntry* entry = nodeIconCache->FindItem(&fNodeRef);
	if (entry == NULL)
		entry = nodeIconCache->AddItem(&fNodeRef);
	if (entry == NULL) {
		delete cacheThumb;
		return B_NO_MEMORY;
	}

	entry->SetIcon(cacheThumb, kNormalIcon, fRequestedSize);
	cacheLocker.Unlock();

	// write values to attributes
	bool thumbnailWritten = false;
	const int32 width = image->Bounds().IntegerWidth() + 1;
	const size_t written = fFile->WriteAttr("Media:Width", B_INT32_TYPE,
		0, &width, sizeof(int32));
	if (written == sizeof(int32)) {
		// first attribute succeeded, write the rest
		const int32 height = image->Bounds().IntegerHeight() + 1;
		fFile->WriteAttr("Media:Height", B_INT32_TYPE, 0, &height, sizeof(int32));

		// convert image into a 128x128 WebP image and stash it
		BBitmap thumb(NULL, false);
		ScaleBitmap(image, thumb, B_XXL_ICON, fColorSpace);

		BBitmap* thumbPointer = &thumb;
		BBitmapStream thumbStream(thumbPointer);
		BMallocIO stream;
		if (BTranslatorRoster::Default()->Translate(&thumbStream,
					NULL, NULL, &stream, B_WEBP_FORMAT) == B_OK
				&& thumbStream.DetachBitmap(&thumbPointer) == B_OK) {
			// write WebP image data into an attribute
			status = fFile->WriteAttr(kAttrThumbnail, B_RAW_TYPE, 0,
				stream.Buffer(), stream.BufferLength());
			thumbnailWritten = (status == B_OK);

			// write thumbnail creation time into an attribute
			int64_t created = real_time_clock();
			fFile->WriteAttr(kAttrThumbnailCreationTime, B_TIME_TYPE,
				0, &created, sizeof(int64_t));
		}
	}

	delete image;

	// Manually trigger an icon refresh, if necessary.
	// (If the attribute was written, node monitoring will handle this automatically.)
	if (!thumbnailWritten) {
		// send Tracker a message to tell it to update the thumbnail
		BMessage message(kUpdateThumbnail);
		if (message.AddNodeRef("noderef", &fNodeRef) == B_OK)
			be_app->PostMessage(&message);
	}

	return B_OK;
}


static status_t
thumbnail_worker(void* castToJobQueue)
{
	JobQueue* queue = (JobQueue*)castToJobQueue;
	while (true) {
		BSupportKit::BJob* job;
		status_t status = queue->Pop(B_INFINITE_TIMEOUT, false, &job);
		if (status == B_INTERRUPTED)
			continue;
		if (status != B_OK)
			break;

		job->Run();
		delete job;
	}

	return B_OK;
}


static status_t
GenerateThumbnail(Model* model, color_space colorSpace, BSize size)
{
	// First check we do not have a job queued already.
	BAutolock jobsLock(sActiveJobsLock);
	for (std::list<GenerateThumbnailJob*>::iterator it = sActiveJobs.begin();
			it != sActiveJobs.end(); it++) {
		if ((*it)->fNodeRef == *model->NodeRef())
			return B_BUSY;
	}
	jobsLock.Unlock();

	BFile* file = dynamic_cast<BFile*>(model->Node());
	if (file == NULL)
		return B_NOT_SUPPORTED;

	struct stat st;
	status_t status = file->GetStat(&st);
	if (status != B_OK)
		return status;

	GenerateThumbnailJob* job = new(std::nothrow) GenerateThumbnailJob(model,
		*file, size, colorSpace);
	ObjectDeleter<GenerateThumbnailJob> jobDeleter(job);
	if (job == NULL)
		return B_NO_MEMORY;
	if (job->InitCheck() != B_OK)
		return job->InitCheck();

	JobQueue** jobQueue;
	if (st.st_size >= (128 * kKBSize)) {
		jobQueue = &sThumbnailWorkers[LARGER_FILES_WORKER];
	} else {
		jobQueue = &sThumbnailWorkers[SMALLER_FILES_WORKER];
	}

	if ((*jobQueue) == NULL) {
		// We need to create the worker.
		*jobQueue = new(std::nothrow) JobQueue();
		if ((*jobQueue) == NULL)
			return B_NO_MEMORY;
		if ((*jobQueue)->InitCheck() != B_OK)
			return (*jobQueue)->InitCheck();
		thread_id thread = spawn_thread(thumbnail_worker, "thumbnail worker",
			B_NORMAL_PRIORITY, *jobQueue);
		if (thread < B_OK)
			return thread;
		resume_thread(thread);
	}

	jobDeleter.Detach();
	status = (*jobQueue)->AddJob(job);
	if (status == B_OK)
		return B_BUSY;

	return status;
}


//	#pragma mark - thumbnail fetching


status_t
GetThumbnailFromAttr(Model* model, BBitmap* icon, BSize size)
{
	if (model == NULL || icon == NULL)
		return B_BAD_VALUE;

	status_t result = model->InitCheck();
	if (result != B_OK)
		return result;

	result = icon->InitCheck();
	if (result != B_OK)
		return result;

	BNode* node = model->Node();
	if (node == NULL)
		return B_BAD_VALUE;

	// look for a thumbnail in an attribute
	time_t modtime;
	int64_t thumbnailCreated;
	if (node->GetModificationTime(&modtime) == B_OK
		&& node->ReadAttr(kAttrThumbnailCreationTime, B_TIME_TYPE, 0,
			&thumbnailCreated, sizeof(int64_t)) == sizeof(int64_t)) {
		if (thumbnailCreated > modtime) {
			// file has not changed, try to return an existing thumbnail
			attr_info attrInfo;
			if (node->GetAttrInfo(kAttrThumbnail, &attrInfo) == B_OK) {
				BMallocIO webpData;
				webpData.SetSize(attrInfo.size);
				if (node->ReadAttr(kAttrThumbnail, attrInfo.type, 0,
						(void*)webpData.Buffer(), attrInfo.size) == attrInfo.size) {
					BBitmap thumb(BTranslationUtils::GetBitmap(&webpData));

					// convert thumb to icon size
					if ((size.IntegerWidth() + 1) == B_XXL_ICON) {
						// import icon data from attribute without resizing
						result = icon->ImportBits(&thumb);
					} else {
						// down-scale thumb to icon size
						// TODO don't make a copy, allow icon to accept views?
						BBitmap tmp(NULL, false);
						ScaleBitmap(&thumb, tmp, icon->Bounds(), icon->ColorSpace());

						// copy tmp bitmap into icon
						result = icon->ImportBits(&tmp);
					}
					// we found a thumbnail
					if (result == B_OK)
						return result;
				}
			}
			// else we did not find a thumbnail
		} else {
			// file changed, remove all thumb attrs
			char attrName[B_ATTR_NAME_LENGTH];
			while (node->GetNextAttrName(attrName) == B_OK) {
				if (BString(attrName).StartsWith(kAttrThumbnail))
					node->RemoveAttr(attrName);
			}
		}
	}

	if (ShouldGenerateThumbnail(model->MimeType()))
		return GenerateThumbnail(model, icon->ColorSpace(), size);

	return B_NOT_SUPPORTED;
}


bool
ShouldGenerateThumbnail(const char* type)
{
	// check generate thumbnail setting,
	// mime type must be an image (for now)
	return TrackerSettings().GenerateImageThumbnails()
		&& type != NULL && BString(type).IStartsWith("image");
}


} // namespace BPrivate