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, 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: 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 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 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 bigtime_t created = real_time_clock_usecs(); 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, 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 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 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 ((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 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