icns

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

readme.md (4544B)


      1 # icns shell extension
      2 
      3 A Windows shell extension that renders thumbnails for `.icns` files in
      4 Explorer. It is an in-process COM server implementing `IInitializeWithStream`
      5 and `IThumbnailProvider`, written in Go.
      6 
      7 ## How it works
      8 
      9 - `internal/com` has the minimal COM plumbing: GUIDs, `HRESULT`s, the
     10   `IUnknown`/`IClassFactory` vtables, an `IStream` → `io.Reader` adapter, and a
     11   generic class factory.
     12 - `internal/provider` is the COM object. Each instance is a Go struct whose
     13   first two fields are vtable pointers (one per interface); the vtables are
     14   package-level singletons filled with `syscall.NewCallback` trampolines.
     15   Instances are reference counted, pinned, and tracked in a global set so the
     16   GC never frees an object the shell still points at.
     17 - `main.go` exports the entry points COM requires (`DllGetClassObject`,
     18   `DllCanUnloadNow`, `DllRegisterServer`, `DllUnregisterServer`, `DllInstall`).
     19   This is the only place cgo appears, and it contains no C code; cgo is needed
     20   purely because `go build -buildmode=c-shared` requires it.
     21 - `register.go` writes the registry entries and notifies the shell.
     22 
     23 Thumbnail selection picks the smallest icon that is at least the requested size
     24 and downsamples it with Lanczos; if none is large enough the largest icon is
     25 returned as-is. The bitmap handed to the shell is a top-down 32bpp DIB with
     26 premultiplied alpha (`WTSAT_ARGB`).
     27 
     28 ## Building
     29 
     30 A GCC-compatible toolchain (mingw-w64) must be on `PATH`; cgo does not support
     31 MSVC. With scoop: `scoop install mingw`.
     32 
     33 ```powershell
     34 go build -buildmode=c-shared -o icns-shellext.dll .
     35 ```
     36 
     37 ## Installing
     38 
     39 Keep the DLL where it is registered from; the absolute path is written to the
     40 registry. There are two modes, chosen by whether `regsvr32` runs elevated:
     41 
     42 | | Per-user (`regsvr32` from a normal prompt) | Machine-wide (`regsvr32` from an elevated prompt) |
     43 |---|---|---|
     44 | Registry | `HKCU\Software\Classes` | `HKLM\SOFTWARE\Classes` + Shell Extensions `Approved` list |
     45 | Hosting | Inside `explorer.exe` (`DisableProcessIsolation=1`) | Isolated `dllhost.exe` surrogate (the shell default) |
     46 | Trade-off | No elevation; a crash in the provider would take Explorer down | Safer hosting; needs admin once |
     47 
     48 ```powershell
     49 regsvr32 icns-shellext.dll      # register
     50 regsvr32 /u icns-shellext.dll   # unregister (run elevated to remove an HKLM registration)
     51 ```
     52 
     53 Why the per-user mode runs in-process: the shell's thumbnail surrogate only
     54 resolves handler CLSIDs from HKLM. A CLSID registered solely under HKCU fails
     55 there with `REGDB_E_CLASSNOTREG` (observed on Windows 11 23H2), and any HKLM
     56 entry for the CLSID, including a stale one pointing at a DLL that no longer
     57 exists, shadows the per-user entry. Elevated registration removes the per-user
     58 entries so the machine-wide, isolated configuration is the one in effect.
     59 
     60 Explorer caches thumbnails, so files viewed before installation may keep their
     61 generic icon until the cache is rebuilt (Disk Cleanup → Thumbnails, or copy the
     62 file). In the isolated mode a replaced DLL only takes effect once the surrogate
     63 exits (a few seconds idle) or you run `taskkill /f /im dllhost.exe`; in the
     64 per-user mode Explorer itself has to be restarted.
     65 
     66 ## Testing
     67 
     68 `go test .` builds the DLL, loads it, and drives it through the raw COM
     69 vtables: class factory, `Initialize` with an in-memory `IStream`, interface
     70 identity, `GetThumbnail` at several sizes, and pixel-level checks of the
     71 returned bitmap (channel order, premultiplied alpha, top-down rows).
     72 
     73 `ICNS_SHELLEXT_E2E=1 go test -run TestExplorer .` additionally asks the shell
     74 itself (`IShellItemImageFactory`) for a thumbnail of a temporary `.icns` file,
     75 which exercises the registry entries and whichever hosting mode is registered.
     76 It requires the DLL to be registered first.
     77 
     78 ## Debugging
     79 
     80 Registration failures are logged to the file named by the `ICNS_SHELLEXT_LOG`
     81 environment variable when set. Thumbnail extraction errors go to the host
     82 process's stderr, which is normally discarded; the in-process test above is the
     83 easiest way to reproduce a decoding problem.
     84 
     85 Useful `HRESULT`s from `IShellItemImageFactory::GetImage(SIIGBF_THUMBNAILONLY)`:
     86 
     87 | Code | Meaning here |
     88 |---|---|
     89 | `0x8004B200` `WTS_E_FAILEDEXTRACTION` | No handler for the extension, or the handler failed to decode |
     90 | `0x80040154` `REGDB_E_CLASSNOTREG` | Handler found but its CLSID is not visible to the host (per-user CLSID in the surrogate) |
     91 | `0x8007007E` `ERROR_MOD_NOT_FOUND` | The registered DLL path does not exist (typically a stale HKLM entry) |