icns

Easily create .icns files (Mac Icons) with this Go library or the included CLI.
Log | Files | Refs | LICENSE

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 )