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.AddInt32("device", fNodeRef.device) == B_OK 241 && message.AddUInt64("node", fNodeRef.node) == B_OK) { 242 be_app->PostMessage(&message); 243 } 244 } 245 246 return B_OK; 247 } 248 249 250 static status_t 251 thumbnail_worker(void* castToJobQueue) 252 { 253 JobQueue* queue = (JobQueue*)castToJobQueue; 254 while (true) { 255 BSupportKit::BJob* job; 256 status_t status = queue->Pop(B_INFINITE_TIMEOUT, false, &job); 257 if (status == B_INTERRUPTED) 258 continue; 259 if (status != B_OK) 260 break; 261 262 job->Run(); 263 delete job; 264 } 265 266 return B_OK; 267 } 268 269 270 static status_t 271 GenerateThumbnail(Model* model, color_space colorSpace, icon_size which) 272 { 273 // First check we do not have a job queued already. 274 BAutolock jobsLock(sActiveJobsLock); 275 for (std::list<GenerateThumbnailJob*>::iterator it = sActiveJobs.begin(); 276 it != sActiveJobs.end(); it++) { 277 if ((*it)->fNodeRef == *model->NodeRef()) 278 return B_BUSY; 279 } 280 jobsLock.Unlock(); 281 282 BFile* file = dynamic_cast<BFile*>(model->Node()); 283 if (file == NULL) 284 return B_NOT_SUPPORTED; 285 286 struct stat st; 287 status_t status = file->GetStat(&st); 288 if (status != B_OK) 289 return status; 290 291 GenerateThumbnailJob* job = new(std::nothrow) GenerateThumbnailJob(model, 292 *file, which, colorSpace); 293 ObjectDeleter<GenerateThumbnailJob> jobDeleter(job); 294 if (job == NULL) 295 return B_NO_MEMORY; 296 if (job->InitCheck() != B_OK) 297 return job->InitCheck(); 298 299 JobQueue** jobQueue; 300 if (st.st_size >= (128 * kKBSize)) { 301 jobQueue = &sThumbnailWorkers[LARGER_FILES_WORKER]; 302 } else { 303 jobQueue = &sThumbnailWorkers[SMALLER_FILES_WORKER]; 304 } 305 306 if ((*jobQueue) == NULL) { 307 // We need to create the worker. 308 *jobQueue = new(std::nothrow) JobQueue(); 309 if ((*jobQueue) == NULL) 310 return B_NO_MEMORY; 311 if ((*jobQueue)->InitCheck() != B_OK) 312 return (*jobQueue)->InitCheck(); 313 thread_id thread = spawn_thread(thumbnail_worker, "thumbnail worker", 314 B_NORMAL_PRIORITY, *jobQueue); 315 if (thread < B_OK) 316 return thread; 317 resume_thread(thread); 318 } 319 320 jobDeleter.Detach(); 321 status = (*jobQueue)->AddJob(job); 322 if (status == B_OK) 323 return B_BUSY; 324 325 return status; 326 } 327 328 329 // #pragma mark - thumbnail fetching 330 331 332 status_t 333 GetThumbnailFromAttr(Model* model, BBitmap* icon, icon_size which) 334 { 335 if (model == NULL || icon == NULL) 336 return B_BAD_VALUE; 337 338 status_t result = model->InitCheck(); 339 if (result != B_OK) 340 return result; 341 342 result = icon->InitCheck(); 343 if (result != B_OK) 344 return result; 345 346 BNode* node = model->Node(); 347 if (node == NULL) 348 return B_BAD_VALUE; 349 350 // look for a thumbnail in an attribute 351 time_t modtime; 352 bigtime_t created; 353 if (node->GetModificationTime(&modtime) == B_OK 354 && node->ReadAttr(kAttrThumbnailCreationTime, B_TIME_TYPE, 0, 355 &created, sizeof(bigtime_t)) == sizeof(bigtime_t)) { 356 if (created > (bigtime_t)modtime) { 357 // file has not changed, try to return an existing thumbnail 358 attr_info attrInfo; 359 if (node->GetAttrInfo(kAttrThumbnail, &attrInfo) == B_OK) { 360 BMallocIO webpData; 361 webpData.SetSize(attrInfo.size); 362 if (node->ReadAttr(kAttrThumbnail, attrInfo.type, 0, 363 (void*)webpData.Buffer(), attrInfo.size) == attrInfo.size) { 364 BBitmap thumb(BTranslationUtils::GetBitmap(&webpData)); 365 366 // convert thumb to icon size 367 if (which == B_XXL_ICON) { 368 // import icon data from attribute without resizing 369 result = icon->ImportBits(&thumb); 370 } else { 371 // down-scale thumb to icon size 372 // TODO don't make a copy, allow icon to accept views? 373 BBitmap tmp(NULL, false); 374 ScaleBitmap(&thumb, tmp, icon->Bounds(), icon->ColorSpace()); 375 376 // copy tmp bitmap into icon 377 result = icon->ImportBits(&tmp); 378 } 379 // we found a thumbnail 380 if (result == B_OK) 381 return result; 382 } 383 } 384 // else we did not find a thumbnail 385 } else { 386 // file changed, remove all thumb attrs 387 char attrName[B_ATTR_NAME_LENGTH]; 388 while (node->GetNextAttrName(attrName) == B_OK) { 389 if (BString(attrName).StartsWith(kAttrThumbnail)) 390 node->RemoveAttr(attrName); 391 } 392 } 393 } 394 395 if (ShouldGenerateThumbnail(model->MimeType())) 396 return GenerateThumbnail(model, icon->ColorSpace(), which); 397 398 return B_NOT_SUPPORTED; 399 } 400 401 402 bool 403 ShouldGenerateThumbnail(const char* type) 404 { 405 // check generate thumbnail setting, 406 // mime type must be an image (for now) 407 return TrackerSettings().GenerateImageThumbnails() 408 && type != NULL && BString(type).IStartsWith("image"); 409 } 410 411 412 } // namespace BPrivate 413