0

I'm writing a QuickLook extension for my app, and I want to be able to display the actual icon image for a particular file, as shown in Finder. I've tried using QLThumbnailGenerator for this, but it will always return a plain white document icon.

QLThumbnailGenerationRequest *request = [[QLThumbnailGenerationRequest alloc] initWithFileAtURL:url size:size scale:scale representationTypes:QLThumbnailGenerationRequestRepresentationTypeIcon];

QLThumbnailGenerator *generator = [QLThumbnailGenerator sharedGenerator];

__unsafe_unretained PreviewViewController *weakSelf = self;

[generator generateBestRepresentationForRequest:request completionHandler:^(QLThumbnailRepresentation *thumbnail, NSError *error) {
    dispatch_async(dispatch_get_main_queue(), ^{
        if (thumbnail == nil || error) {
            NSLog(@"Failed to generate thumbnail! %@", error);
            // Handle the error case gracefully
        } else {
            // Display the thumbnail that you created
            NSLog(@"Generated thumbnail!");
            weakSelf.imageView.image = thumbnail.NSImage;
        }
    });
}];

Original code here.

2 Answers2

2

2022 SwiftUI, MacOs

import SwiftUI
import QuickLookThumbnailing

struct ThumbnailImageView: View {
    let url: URL
    let size: CGFloat

    @State private var thumbnail: NSImage? = nil

    var body: some View {
        Group {
            if let thumbnail = thumbnail {
                Image(nsImage: thumbnail)
            } else {
                Image(systemName: "photo") // << any placeholder
                  .onAppear(perform: generateThumbnail) // << here !!
            }
        }
    }

    func generateThumbnail() {
        let size: CGSize = CGSize(width: size, height: size)
        let request = QLThumbnailGenerator.Request(fileAt: url, size: size, scale: 1.0, representationTypes: .lowQualityThumbnail)
            request.iconMode = true
        let generator = QLThumbnailGenerator.shared
        
        generator.generateRepresentations(for: request) { (thumbnail, type, error) in
            DispatchQueue.main.async {
                if thumbnail == nil || error != nil {
                    assert(false, "Thumbnail failed to generate")
                } else {
                    DispatchQueue.main.async { // << required !!
                        self.thumbnail = thumbnail!.nsImage  // here !!
                    }
                }
            }
        }
    }
}

usage:

ThumbnailImageView(url: url, size: 100)

and another way:

import Foundation
import SwiftUI
import Quartz
import QuickLook

struct UKSImage: View {
    let url: URL
    let size: CGFloat
    
    @State private var thumbnail: NSImage? = nil
    
    var body: some View {
        if let thumbnail = thumbnail {
            Image(nsImage: thumbnail)
                .resizable()
                .scaledToFit()
        } else {
            Image(systemName: "photo") // << any placeholder
              .onAppear(perform: generateThumbnail) // << here !!
        }
    }
    
    func generateThumbnail() {
        DispatchQueue.global(qos: .background).async {
            self.thumbnail = url.imgThumbnailAdv(size)
        }
    }
}


fileprivate extension URL {
    func imgThumbnailAdv(_ size: CGFloat) -> NSImage? {
        let extensionsExceptions: [String]  = ["txt","docx","doc","pages","odt","rtf","tex","wpd","ltxd",
                                               "btxt","dotx","wtt","dsc","me","ans","log","xy","text","docm",
                                               "wps","rst","readme","asc","strings","docz","docxml","sdoc",
                                               "plain","notes","latex","utxt","ascii",

                                               "xlsx","patch","xls","xlsm","ods",

                                               "py","cs","swift","html","css", "fountain","gscript","lua",

                                               "markdown","md",
                                               "plist"
        ]

        if extensionsExceptions.contains(self.pathExtension.lowercased()) {
            let img = NSWorkspace.shared.highResIcon(forPath: self.path, resolution: Int(size))

            return img
        }
        
        if let img2 = self.getImgThumbnail(size) {
            return img2
        }
        
        return NSWorkspace.shared.highResIcon(forPath: self.path, resolution: Int(size))
    }
}

extension NSImage{
    var pixelSize: NSSize?{
        if let rep = self.representations.first{
            let size = NSSize(width: rep.pixelsWide, height: rep.pixelsHigh)
            return size
        }
        return nil
    }
}

fileprivate extension URL {
    func getImgThumbnail(_ size: CGFloat) -> NSImage? {
        let ref = QLThumbnailCreate ( kCFAllocatorDefault,
                                      self as NSURL,
                                      CGSize(width: size, height: size),
                                      [ kQLThumbnailOptionIconModeKey: false ] as CFDictionary
        )
        
        guard let thumbnail = ref?.takeRetainedValue()
        else { return nil }
        
        if let cgImageRef = QLThumbnailCopyImage(thumbnail) {
            let cgImage = cgImageRef.takeRetainedValue()
            return NSImage(cgImage: cgImage, size: CGSize(width: cgImage.width, height: cgImage.height))
        }
        
        return nil
    }
}

extension NSWorkspace {
    func highResIcon(forPath path: String, resolution: Int = 512) -> NSImage? {
        if let rep = self.icon(forFile: path)
            .bestRepresentation(for: NSRect(x: 0, y: 0, width: resolution, height: resolution), context: nil, hints: nil) {
            let image = NSImage(size: rep.size)
            image.addRepresentation(rep)
            return image
        }
        return nil
    }
}

usage:

UKSImage(url: url, size: 100)
Andrew_STOP_RU_WAR_IN_UA
  • 9,318
  • 5
  • 65
  • 101
-1

This is more of a workaround, but using the iconForFile method instead, i.e. _imageView.image = [[NSWorkspace sharedWorkspace] iconForFile:url.path] does what I want.