provider.go (9150B)
1 //go:build windows 2 3 // Package provider implements the icns thumbnail provider COM object: a single 4 // class exposing IInitializeWithStream (to receive the .icns bytes) and 5 // IThumbnailProvider (to hand Explorer an HBITMAP). 6 package provider 7 8 import ( 9 "bytes" 10 "fmt" 11 "image" 12 "io" 13 "log/slog" 14 "runtime" 15 "sync" 16 "sync/atomic" 17 "syscall" 18 "unsafe" 19 20 "github.com/jackmordaunt/icns/cmd/shell-extension/internal/com" 21 "github.com/jackmordaunt/icns/v4" 22 "golang.org/x/image/draw" 23 "golang.org/x/sys/windows" 24 ) 25 26 // CLSID identifies this thumbnail provider class in the registry. 27 var CLSID = com.MustGUID("{E21C95C5-5086-4F9F-8876-7FF4CE4AC6EC}") 28 29 // Interface identifiers implemented by Provider. 30 var ( 31 IID_IInitializeWithStream = com.MustGUID("{B824B49D-22AC-4161-AC8A-9916E8FA3F7F}") 32 IID_IThumbnailProvider = com.MustGUID("{E357FCCD-A995-4576-B01F-234630154E96}") 33 ) 34 35 // WTS_ALPHATYPE values reported through IThumbnailProvider::GetThumbnail. 36 const ( 37 WTSAT_UNKNOWN uint32 = 0 38 WTSAT_RGB uint32 = 1 39 WTSAT_ARGB uint32 = 2 40 ) 41 42 type initializeWithStreamVtbl struct { 43 com.IUnknownVtbl 44 Initialize uintptr // HRESULT (*)(This, IStream *pstream, DWORD grfMode) 45 } 46 47 type thumbnailProviderVtbl struct { 48 com.IUnknownVtbl 49 GetThumbnail uintptr // HRESULT (*)(This, UINT cx, HBITMAP *phbmp, WTS_ALPHATYPE *pdwAlpha) 50 } 51 52 // Provider is one instance of the thumbnail provider; the shell creates one per 53 // file it wants a thumbnail for. 54 // 55 // The first two fields are the interface pointers handed to COM. The address of 56 // the struct doubles as the IUnknown/IInitializeWithStream pointer, and the 57 // address of thumbVtbl is the IThumbnailProvider pointer. The trampolines for 58 // the second interface subtract that field's offset to recover the Provider. 59 type Provider struct { 60 initVtbl *initializeWithStreamVtbl 61 thumbVtbl *thumbnailProviderVtbl 62 63 refs atomic.Int32 64 pin runtime.Pinner 65 66 mu sync.Mutex 67 data []byte // raw .icns bytes supplied by Initialize 68 } 69 70 // live tracks every Provider COM still holds a reference to. This keeps the 71 // objects reachable from Go while the only pointers to them are in the shell. 72 var live = struct { 73 sync.Mutex 74 set map[*Provider]struct{} 75 }{set: map[*Provider]struct{}{}} 76 77 // Live reports the number of outstanding Provider instances. 78 func Live() int { 79 live.Lock() 80 defer live.Unlock() 81 return len(live.set) 82 } 83 84 // New allocates a Provider with a reference count of one. 85 func New() *Provider { 86 p := &Provider{initVtbl: initVtbl, thumbVtbl: thumbVtbl} 87 p.refs.Store(1) 88 p.pin.Pin(p) 89 live.Lock() 90 live.set[p] = struct{}{} 91 live.Unlock() 92 return p 93 } 94 95 // Create is a [com.Constructor] for use with [com.NewClassFactory]. 96 func Create(riid *com.GUID, ppv *unsafe.Pointer) com.HRESULT { 97 p := New() 98 // QueryInterface takes the caller's reference; drop the constructor's. 99 defer p.Release() 100 return p.QueryInterface(riid, ppv) 101 } 102 103 // QueryInterface implements IUnknown::QueryInterface. 104 func (p *Provider) QueryInterface(riid *com.GUID, ppv *unsafe.Pointer) com.HRESULT { 105 if ppv == nil { 106 return com.E_POINTER 107 } 108 switch { 109 case com.IsEqualGUID(riid, com.IID_IUnknown), com.IsEqualGUID(riid, IID_IInitializeWithStream): 110 *ppv = unsafe.Pointer(p) 111 case com.IsEqualGUID(riid, IID_IThumbnailProvider): 112 *ppv = unsafe.Pointer(&p.thumbVtbl) 113 default: 114 *ppv = nil 115 return com.E_NOINTERFACE 116 } 117 p.AddRef() 118 return com.S_OK 119 } 120 121 // AddRef implements IUnknown::AddRef. 122 func (p *Provider) AddRef() uint32 { 123 return uint32(p.refs.Add(1)) 124 } 125 126 // Release implements IUnknown::Release, freeing the object on the last release. 127 func (p *Provider) Release() uint32 { 128 n := p.refs.Add(-1) 129 if n == 0 { 130 live.Lock() 131 delete(live.set, p) 132 live.Unlock() 133 p.mu.Lock() 134 p.data = nil 135 p.mu.Unlock() 136 p.pin.Unpin() 137 } 138 return uint32(n) 139 } 140 141 // Initialize implements IInitializeWithStream::Initialize by buffering the 142 // whole stream. The stream is only guaranteed valid for the duration of this 143 // call, and icns decoding needs the entire file anyway. 144 func (p *Provider) Initialize(stream *com.IStream, grfMode uint32) com.HRESULT { 145 if stream == nil { 146 return com.E_POINTER 147 } 148 p.mu.Lock() 149 defer p.mu.Unlock() 150 if p.data != nil { 151 return com.HRESULT_ALREADY_INITIALIZED 152 } 153 data, err := io.ReadAll(stream) 154 if err != nil { 155 slog.Error("reading icns stream", "err", err) 156 return com.E_FAIL 157 } 158 p.data = data 159 return com.S_OK 160 } 161 162 // GetThumbnail implements IThumbnailProvider::GetThumbnail. The returned bitmap 163 // is a top-down 32bpp DIB with premultiplied alpha; ownership passes to the 164 // caller. 165 func (p *Provider) GetThumbnail(cx uint32, phbmp *windows.Handle, pdwAlpha *uint32) com.HRESULT { 166 if phbmp == nil || pdwAlpha == nil { 167 return com.E_POINTER 168 } 169 *phbmp = 0 170 *pdwAlpha = WTSAT_UNKNOWN 171 172 p.mu.Lock() 173 data := p.data 174 p.mu.Unlock() 175 if data == nil { 176 return com.E_UNEXPECTED // Initialize was never called. 177 } 178 179 img, err := Thumbnail(bytes.NewReader(data), int(cx)) 180 if err != nil { 181 slog.Error("extracting icns thumbnail", "cx", cx, "err", err) 182 return com.WTS_E_FAILEDEXTRACTION 183 } 184 hbmp, err := CreateDIB(img) 185 if err != nil { 186 slog.Error("creating thumbnail bitmap", "err", err) 187 return com.E_FAIL 188 } 189 *phbmp = hbmp 190 *pdwAlpha = WTSAT_ARGB 191 return com.S_OK 192 } 193 194 // Thumbnail decodes the icns and returns the icon best suited to a cx by cx 195 // square: the smallest icon at least that large, downscaled to fit, or the 196 // largest available icon when none is big enough. 197 func Thumbnail(r io.Reader, cx int) (*image.RGBA, error) { 198 if cx <= 0 { 199 return nil, fmt.Errorf("invalid thumbnail size %d", cx) 200 } 201 d, err := icns.NewDecoder(r) 202 if err != nil { 203 return nil, err 204 } 205 // The icons arrive largest first, so the last one still at least cx wide 206 // is the smallest that does not need upscaling. Only that one is decoded. 207 var ( 208 best icns.Entry 209 found bool 210 ) 211 for _, icon := range d.Icons() { 212 if icon.ImageFormat == icns.ImageFormatJPEG2000 { 213 continue 214 } 215 if found && int(icon.Size) < cx { 216 break 217 } 218 best, found = icon, true 219 } 220 if !found { 221 return nil, fmt.Errorf("no icon in a format this build can decode") 222 } 223 chosen, err := best.Decode() 224 if err != nil { 225 return nil, err 226 } 227 // Normalise to premultiplied RGBA, which is what a GDI ARGB bitmap 228 // wants, shrinking the icon to fit the cx square on the way if needed. 229 var ( 230 b = chosen.Bounds() 231 w, h = b.Dx(), b.Dy() 232 ) 233 if side(chosen) > cx { 234 if w >= h { 235 w, h = cx, max(h*cx/w, 1) 236 } else { 237 w, h = max(w*cx/h, 1), cx 238 } 239 } 240 rgba := image.NewRGBA(image.Rect(0, 0, w, h)) 241 if w == b.Dx() && h == b.Dy() { 242 draw.Draw(rgba, rgba.Bounds(), chosen, b.Min, draw.Src) 243 } else { 244 // CatmullRom is x/image's high quality kernel. 245 draw.CatmullRom.Scale(rgba, rgba.Bounds(), chosen, b, draw.Src, nil) 246 } 247 return rgba, nil 248 } 249 250 func side(img image.Image) int { 251 s := img.Bounds().Size() 252 return max(s.X, s.Y) 253 } 254 255 // fromThumb recovers the Provider from an IThumbnailProvider `this` pointer. 256 func fromThumb(this unsafe.Pointer) *Provider { 257 return (*Provider)(unsafe.Add(this, -int(unsafe.Offsetof(Provider{}.thumbVtbl)))) 258 } 259 260 // Shared vtables. Every trampoline must return a single uintptr, and every 261 // body runs under com.Guard so a panic (a malformed file, a GDI failure) 262 // surfaces as E_FAIL instead of unwinding into the host process. 263 var ( 264 initVtbl = &initializeWithStreamVtbl{ 265 IUnknownVtbl: com.IUnknownVtbl{ 266 QueryInterface: syscall.NewCallback(func(this *Provider, riid *com.GUID, ppv *unsafe.Pointer) uintptr { 267 return com.Guard("IInitializeWithStream::QueryInterface", func() com.HRESULT { return this.QueryInterface(riid, ppv) }) 268 }), 269 AddRef: syscall.NewCallback(func(this *Provider) uintptr { 270 return com.Guard("IInitializeWithStream::AddRef", func() com.HRESULT { return uintptr(this.AddRef()) }) 271 }), 272 Release: syscall.NewCallback(func(this *Provider) uintptr { 273 return com.Guard("IInitializeWithStream::Release", func() com.HRESULT { return uintptr(this.Release()) }) 274 }), 275 }, 276 Initialize: syscall.NewCallback(func(this *Provider, stream *com.IStream, grfMode uint32) uintptr { 277 return com.Guard("IInitializeWithStream::Initialize", func() com.HRESULT { return this.Initialize(stream, grfMode) }) 278 }), 279 } 280 281 thumbVtbl = &thumbnailProviderVtbl{ 282 IUnknownVtbl: com.IUnknownVtbl{ 283 QueryInterface: syscall.NewCallback(func(this unsafe.Pointer, riid *com.GUID, ppv *unsafe.Pointer) uintptr { 284 return com.Guard("IThumbnailProvider::QueryInterface", func() com.HRESULT { return fromThumb(this).QueryInterface(riid, ppv) }) 285 }), 286 AddRef: syscall.NewCallback(func(this unsafe.Pointer) uintptr { 287 return com.Guard("IThumbnailProvider::AddRef", func() com.HRESULT { return uintptr(fromThumb(this).AddRef()) }) 288 }), 289 Release: syscall.NewCallback(func(this unsafe.Pointer) uintptr { 290 return com.Guard("IThumbnailProvider::Release", func() com.HRESULT { return uintptr(fromThumb(this).Release()) }) 291 }), 292 }, 293 GetThumbnail: syscall.NewCallback(func(this unsafe.Pointer, cx uint32, phbmp *windows.Handle, pdwAlpha *uint32) uintptr { 294 return com.Guard("IThumbnailProvider::GetThumbnail", func() com.HRESULT { return fromThumb(this).GetThumbnail(cx, phbmp, pdwAlpha) }) 295 }), 296 } 297 )