thumbnail preview fixes windows (#6074)

This commit is contained in:
Anthony Stirling
2026-04-15 23:25:38 +01:00
committed by GitHub
parent cc5a0b8def
commit 60c036e980
9 changed files with 684 additions and 6 deletions

View File

@@ -282,8 +282,9 @@ public class ProcessExecutor {
boolean finished = process.waitFor(timeoutDuration, TimeUnit.MINUTES);
if (!finished) {
// Terminate the process
process.destroy();
// Kill the entire process tree (descendants first, then the process itself)
process.descendants().forEach(ProcessHandle::destroyForcibly);
process.destroyForcibly();
// Interrupt the reader threads
errorReaderThread.interrupt();
outputReaderThread.interrupt();

View File

@@ -5,10 +5,6 @@ services:
build:
context: ../../../
dockerfile: docker/embedded/Dockerfile.fat
deploy:
resources:
limits:
memory: 4G
healthcheck:
test: ["CMD-SHELL", "curl -f -H 'X-API-KEY: 123456789' http://localhost:8080${SYSTEM_ROOTURIPATH:-''}/api/v1/info/status | grep -q 'UP'"]
interval: 5s

View File

@@ -22,3 +22,15 @@ mkdirSync(wixDir, { recursive: true });
const destExe = join(wixDir, "stirling-provision.exe");
copyFileSync(provisionerExe, destExe);
// --- Thumbnail handler DLL ---
const thumbManifest = join(tauriDir, "thumbnail-handler", "Cargo.toml");
execFileSync("cargo", ["build", "--release", "--manifest-path", thumbManifest], { stdio: "inherit" });
const thumbDll = join(tauriDir, "thumbnail-handler", "target", "release", "stirling_thumbnail_handler.dll");
if (!existsSync(thumbDll)) {
throw new Error(`Thumbnail handler DLL not found at ${thumbDll}`);
}
copyFileSync(thumbDll, join(wixDir, "stirling_thumbnail_handler.dll"));

View File

@@ -0,0 +1 @@
/target

View File

@@ -0,0 +1,174 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "proc-macro2"
version = "1.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
dependencies = [
"proc-macro2",
]
[[package]]
name = "stirling-thumbnail-handler"
version = "0.1.0"
dependencies = [
"windows",
"windows-core",
]
[[package]]
name = "syn"
version = "2.0.117"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "unicode-ident"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
name = "windows"
version = "0.58.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6"
dependencies = [
"windows-core",
"windows-targets",
]
[[package]]
name = "windows-core"
version = "0.58.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99"
dependencies = [
"windows-implement",
"windows-interface",
"windows-result",
"windows-strings",
"windows-targets",
]
[[package]]
name = "windows-implement"
version = "0.58.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "windows-interface"
version = "0.58.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "windows-result"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-strings"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10"
dependencies = [
"windows-result",
"windows-targets",
]
[[package]]
name = "windows-targets"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_gnullvm",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"

View File

@@ -0,0 +1,26 @@
[package]
name = "stirling-thumbnail-handler"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies.windows]
version = "0.58"
features = [
"implement",
"Win32_Foundation",
"Win32_System_Com",
"Win32_System_Com_StructuredStorage",
"Win32_Graphics_Gdi",
"Win32_Graphics_Imaging",
"Win32_UI_Shell",
"Win32_UI_Shell_PropertiesSystem",
"Storage_Streams",
"Data_Pdf",
"Foundation",
]
[dependencies]
windows-core = "0.58"

View File

@@ -0,0 +1,61 @@
# Windows PDF Thumbnail Handler
A lightweight COM DLL that provides PDF page-preview thumbnails in Windows Explorer when Stirling-PDF is the default PDF application.
## Why this exists
When Stirling-PDF registers as the default PDF handler, Windows associates `.pdf` files with Stirling's ProgID. Without a thumbnail handler on that ProgID, Explorer falls back to showing the application icon (the big S logo) instead of a page preview. This DLL restores thumbnail previews by implementing the Windows Shell `IThumbnailProvider` COM interface.
## How it works
1. **Explorer requests a thumbnail** — when a folder with PDFs is opened in Medium/Large icon view, Explorer loads the DLL via the registered COM CLSID.
2. **Shell calls `IInitializeWithStream`** — passes the PDF file content as an `IStream`.
3. **Shell calls `IThumbnailProvider::GetThumbnail(cx)`** — requests a bitmap of size `cx × cx`.
4. **The DLL renders page 1** using the built-in `Windows.Data.Pdf` WinRT API (the same engine Edge uses), preserving aspect ratio.
5. **WIC decodes the rendered PNG** into BGRA pixels, which are copied into an `HBITMAP` via `CreateDIBSection`.
6. **Explorer displays the bitmap** as the file's thumbnail.
All COM methods are wrapped in `catch_unwind` so a malformed PDF cannot crash Explorer.
## Technical details
| | |
|---|---|
| **Language** | Rust (cdylib) |
| **DLL size** | ~156 KB |
| **External deps** | None — uses only Windows built-in APIs |
| **PDF renderer** | `Windows.Data.Pdf` (WinRT, Windows 10+) |
| **Image decode** | WIC (`IWICImagingFactory`) with BGRA32 format conversion |
| **COM CLSID** | `{2D2FBE3A-9A88-4308-A52E-7EF63CA7CF48}` |
| **Threading model** | Apartment (STA — standard for shell extensions) |
| **Min Windows** | Windows 10 |
## Registry entries (managed by MSI)
The WiX installer (`provisioning.wxs`) registers:
- **CLSID** at `HKLM\SOFTWARE\Classes\CLSID\{2D2FBE3A-...}\InprocServer32` pointing to the DLL
- **Shellex** at `HKLM\SOFTWARE\Classes\.pdf\shellex\{E357FCCD-...}` linking `.pdf` thumbnails to our CLSID
Both are automatically removed on uninstall.
## Building
The DLL is built automatically as part of the Tauri build pipeline via `build-provisioner.mjs`:
```bash
cd frontend
npm run tauri-build
```
To build the DLL standalone:
```bash
cd frontend/src-tauri/thumbnail-handler
cargo build --release
# Output: target/release/stirling_thumbnail_handler.dll
```
## Linux / macOS
This DLL is Windows-only. Linux and macOS don't need it — their thumbnail systems (thumbnailers on Linux, Quick Look on macOS) are decoupled from the default app association and continue working regardless of which app is set as default.

View File

@@ -0,0 +1,383 @@
//! Stirling-PDF Windows Thumbnail Handler
//!
//! A lightweight COM DLL that implements IThumbnailProvider for PDF files.
//! Uses the built-in Windows.Data.Pdf WinRT API to render page 1 as a thumbnail.
use std::cell::RefCell;
use std::ffi::c_void;
use std::panic::catch_unwind;
use std::sync::atomic::{AtomicU32, Ordering};
use windows::core::{implement, IUnknown, Interface, GUID, HRESULT};
use windows::Win32::Foundation::{
BOOL, CLASS_E_CLASSNOTAVAILABLE, CLASS_E_NOAGGREGATION, E_FAIL, E_UNEXPECTED, S_FALSE, S_OK,
};
use windows::Win32::Graphics::Gdi::{
CreateDIBSection, BITMAPINFO, BITMAPINFOHEADER, BI_RGB, DIB_RGB_COLORS, HBITMAP,
};
use windows::Win32::Graphics::Imaging::{
CLSID_WICImagingFactory, GUID_WICPixelFormat32bppBGRA, IWICImagingFactory,
WICBitmapDitherTypeNone, WICBitmapPaletteTypeCustom, WICDecodeMetadataCacheOnDemand,
};
use windows::Win32::System::Com::{
CoCreateInstance, IClassFactory, IClassFactory_Impl, IStream, CLSCTX_INPROC_SERVER,
STATFLAG_DEFAULT, STREAM_SEEK_SET,
};
use windows::Win32::UI::Shell::{
IThumbnailProvider, IThumbnailProvider_Impl, SHCreateMemStream, WTS_ALPHATYPE,
};
use windows::Win32::UI::Shell::PropertiesSystem::{
IInitializeWithStream, IInitializeWithStream_Impl,
};
// WinRT imports for PDF rendering
use windows::Data::Pdf::PdfDocument;
use windows::Storage::Streams::{DataWriter, InMemoryRandomAccessStream, IRandomAccessStream};
// CLSID for this thumbnail handler -- must match WiX registry entries
const CLSID_STIRLING_THUMBNAIL: GUID = GUID::from_u128(0x2d2fbe3a_9a88_4308_a52e_7ef63ca7cf48);
static DLL_REF_COUNT: AtomicU32 = AtomicU32::new(0);
// Maximum PDF size we'll attempt to thumbnail (256 MB)
const MAX_PDF_SIZE: usize = 256 * 1024 * 1024;
// ---------------------------------------------------------------------------
// ThumbnailProvider -- the COM object
// ---------------------------------------------------------------------------
#[implement(IThumbnailProvider, IInitializeWithStream)]
struct ThumbnailProvider {
stream: RefCell<Option<IStream>>,
}
impl ThumbnailProvider {
fn new() -> Self {
DLL_REF_COUNT.fetch_add(1, Ordering::SeqCst);
Self {
stream: RefCell::new(None),
}
}
}
impl Drop for ThumbnailProvider {
fn drop(&mut self) {
DLL_REF_COUNT.fetch_sub(1, Ordering::SeqCst);
}
}
impl IInitializeWithStream_Impl for ThumbnailProvider_Impl {
fn Initialize(
&self,
pstream: Option<&IStream>,
_grfmode: u32,
) -> windows::core::Result<()> {
let result = catch_unwind(std::panic::AssertUnwindSafe(|| {
*self.stream.borrow_mut() = pstream.cloned();
}));
match result {
Ok(()) => Ok(()),
Err(_) => Err(E_UNEXPECTED.into()),
}
}
}
impl IThumbnailProvider_Impl for ThumbnailProvider_Impl {
fn GetThumbnail(
&self,
cx: u32,
phbmp: *mut HBITMAP,
pdwalpha: *mut WTS_ALPHATYPE,
) -> windows::core::Result<()> {
let result = catch_unwind(std::panic::AssertUnwindSafe(|| {
self.get_thumbnail_inner(cx, phbmp, pdwalpha)
}));
match result {
Ok(inner) => inner,
Err(_) => Err(E_UNEXPECTED.into()),
}
}
}
impl ThumbnailProvider_Impl {
fn get_thumbnail_inner(
&self,
cx: u32,
phbmp: *mut HBITMAP,
pdwalpha: *mut WTS_ALPHATYPE,
) -> windows::core::Result<()> {
let stream = self.stream.borrow();
let stream = stream.as_ref().ok_or(E_FAIL)?;
// Step 1: Read the IStream into a byte buffer
let bytes = read_istream_to_vec(stream)?;
if bytes.is_empty() {
return Err(E_FAIL.into());
}
// Step 2: Load the PDF via WinRT
let winrt_stream = bytes_to_random_access_stream(&bytes)?;
let pdf_doc = PdfDocument::LoadFromStreamAsync(&winrt_stream)?.get()?;
if pdf_doc.PageCount()? == 0 {
return Err(E_FAIL.into());
}
let page = pdf_doc.GetPage(0)?;
// Step 3: Render page 1 to a PNG stream
let output_stream = InMemoryRandomAccessStream::new()?;
let render_options = windows::Data::Pdf::PdfPageRenderOptions::new()?;
// Calculate dimensions preserving aspect ratio
let page_size = page.Size()?;
let scale = cx as f64 / f64::max(page_size.Width as f64, page_size.Height as f64);
let render_w = (page_size.Width as f64 * scale).max(1.0) as u32;
let render_h = (page_size.Height as f64 * scale).max(1.0) as u32;
render_options.SetDestinationWidth(render_w)?;
render_options.SetDestinationHeight(render_h)?;
page.RenderWithOptionsToStreamAsync(&output_stream, &render_options)?
.get()?;
// Step 4: Decode the PNG using WIC -> raw BGRA pixels -> HBITMAP
let hbitmap = png_stream_to_hbitmap(&output_stream, render_w, render_h)?;
// Step 5: Return the HBITMAP
unsafe {
*phbmp = hbitmap;
// WTSAT_ARGB = 2
*pdwalpha = WTS_ALPHATYPE(2);
}
Ok(())
}
}
// ---------------------------------------------------------------------------
// Helper: read IStream to Vec<u8> (with loop for short reads)
// ---------------------------------------------------------------------------
fn read_istream_to_vec(stream: &IStream) -> windows::core::Result<Vec<u8>> {
unsafe {
// Get stream size
let mut stat = std::mem::zeroed();
stream.Stat(&mut stat, STATFLAG_DEFAULT)?;
let size = stat.cbSize as usize;
if size == 0 {
return Ok(Vec::new());
}
if size > MAX_PDF_SIZE {
return Err(E_FAIL.into());
}
// Seek to beginning
stream.Seek(0, STREAM_SEEK_SET, None)?;
// Read all bytes, looping for short reads
let mut buffer = vec![0u8; size];
let mut total_read = 0usize;
while total_read < size {
let mut bytes_read = 0u32;
stream
.Read(
buffer[total_read..].as_mut_ptr() as *mut c_void,
(size - total_read) as u32,
Some(&mut bytes_read),
)
.ok()?;
if bytes_read == 0 {
break;
}
total_read += bytes_read as usize;
}
buffer.truncate(total_read);
Ok(buffer)
}
}
// ---------------------------------------------------------------------------
// Helper: bytes -> WinRT IRandomAccessStream
// ---------------------------------------------------------------------------
fn bytes_to_random_access_stream(bytes: &[u8]) -> windows::core::Result<IRandomAccessStream> {
let mem_stream = InMemoryRandomAccessStream::new()?;
let writer = DataWriter::CreateDataWriter(&mem_stream)?;
writer.WriteBytes(bytes)?;
writer.StoreAsync()?.get()?;
// Detach the writer so it doesn't close the stream
writer.DetachStream()?;
// Seek back to beginning
mem_stream.Seek(0)?;
Ok(mem_stream.cast()?)
}
// ---------------------------------------------------------------------------
// Helper: PNG stream -> HBITMAP via WIC (with format conversion to BGRA32)
// ---------------------------------------------------------------------------
fn png_stream_to_hbitmap(
winrt_stream: &InMemoryRandomAccessStream,
width: u32,
height: u32,
) -> windows::core::Result<HBITMAP> {
unsafe {
// Seek to beginning and read PNG data
winrt_stream.Seek(0)?;
let size = winrt_stream.Size()? as usize;
if size == 0 {
return Err(E_FAIL.into());
}
let reader = windows::Storage::Streams::DataReader::CreateDataReader(
&winrt_stream.GetInputStreamAt(0)?,
)?;
reader.LoadAsync(size as u32)?.get()?;
let mut png_bytes = vec![0u8; size];
reader.ReadBytes(&mut png_bytes)?;
// Create a COM IStream from the PNG bytes
let png_stream = SHCreateMemStream(Some(&png_bytes)).ok_or(E_FAIL)?;
// Create WIC factory and decode the PNG
let wic_factory: IWICImagingFactory =
CoCreateInstance(&CLSID_WICImagingFactory, None, CLSCTX_INPROC_SERVER)?;
let decoder = wic_factory.CreateDecoderFromStream(
&png_stream,
std::ptr::null(),
WICDecodeMetadataCacheOnDemand,
)?;
let frame = decoder.GetFrame(0)?;
// Convert to BGRA32 to ensure consistent pixel format
let converter = wic_factory.CreateFormatConverter()?;
converter.Initialize(
&frame,
&GUID_WICPixelFormat32bppBGRA,
WICBitmapDitherTypeNone,
None,
0.0,
WICBitmapPaletteTypeCustom,
)?;
// Read pixels as BGRA
let stride = width * 4;
let buf_size = (stride * height) as usize;
let mut pixels = vec![0u8; buf_size];
converter.CopyPixels(std::ptr::null(), stride, &mut pixels)?;
// Create a DIB section HBITMAP
let bmi = BITMAPINFO {
bmiHeader: BITMAPINFOHEADER {
biSize: std::mem::size_of::<BITMAPINFOHEADER>() as u32,
biWidth: width as i32,
biHeight: -(height as i32), // top-down
biPlanes: 1,
biBitCount: 32,
biCompression: BI_RGB.0,
biSizeImage: 0,
biXPelsPerMeter: 0,
biYPelsPerMeter: 0,
biClrUsed: 0,
biClrImportant: 0,
},
bmiColors: [std::mem::zeroed()],
};
let mut bits: *mut c_void = std::ptr::null_mut();
let hbitmap = CreateDIBSection(None, &bmi, DIB_RGB_COLORS, &mut bits, None, 0)?;
if bits.is_null() {
return Err(E_FAIL.into());
}
// Copy pixel data into the DIB section
std::ptr::copy_nonoverlapping(pixels.as_ptr(), bits as *mut u8, buf_size);
Ok(hbitmap)
}
}
// ---------------------------------------------------------------------------
// ClassFactory
// ---------------------------------------------------------------------------
#[implement(IClassFactory)]
struct ThumbnailProviderFactory;
impl IClassFactory_Impl for ThumbnailProviderFactory_Impl {
fn CreateInstance(
&self,
punkouter: Option<&IUnknown>,
riid: *const GUID,
ppvobject: *mut *mut c_void,
) -> windows::core::Result<()> {
unsafe {
*ppvobject = std::ptr::null_mut();
}
if punkouter.is_some() {
return Err(CLASS_E_NOAGGREGATION.into());
}
let provider = ThumbnailProvider::new();
let unknown: IUnknown = provider.into();
unsafe { unknown.query(&*riid, ppvobject).ok() }
}
fn LockServer(&self, flock: BOOL) -> windows::core::Result<()> {
if flock.as_bool() {
DLL_REF_COUNT.fetch_add(1, Ordering::SeqCst);
} else {
DLL_REF_COUNT.fetch_sub(1, Ordering::SeqCst);
}
Ok(())
}
}
// ---------------------------------------------------------------------------
// DLL exports
// ---------------------------------------------------------------------------
#[no_mangle]
unsafe extern "system" fn DllGetClassObject(
rclsid: *const GUID,
riid: *const GUID,
ppv: *mut *mut c_void,
) -> HRESULT {
if ppv.is_null() {
return E_FAIL;
}
*ppv = std::ptr::null_mut();
if *rclsid != CLSID_STIRLING_THUMBNAIL {
return CLASS_E_CLASSNOTAVAILABLE;
}
let factory = ThumbnailProviderFactory;
let unknown: IUnknown = factory.into();
match unknown.query(&*riid, ppv).ok() {
Ok(()) => S_OK,
Err(e) => e.into(),
}
}
#[no_mangle]
extern "system" fn DllCanUnloadNow() -> HRESULT {
if DLL_REF_COUNT.load(Ordering::SeqCst) == 0 {
S_OK
} else {
S_FALSE
}
}

View File

@@ -13,10 +13,34 @@
<Component Id="ProvisionerBinaryComponent" Guid="*">
<File Id="ProvisionerExe" Source="$(sys.SOURCEFILEDIR)stirling-provision.exe" KeyPath="yes" />
</Component>
<Component Id="ThumbnailHandlerDllComponent" Guid="*">
<File Id="ThumbnailHandlerDll" Source="$(sys.SOURCEFILEDIR)stirling_thumbnail_handler.dll" KeyPath="yes" />
</Component>
</DirectoryRef>
<!-- PDF Thumbnail Handler COM registration -->
<DirectoryRef Id="TARGETDIR">
<Component Id="ThumbnailHandlerClsidComponent" Guid="*">
<RegistryKey Root="HKLM" Key="SOFTWARE\Classes\CLSID\{2D2FBE3A-9A88-4308-A52E-7EF63CA7CF48}">
<RegistryValue Type="string" Value="Stirling-PDF Thumbnail Handler" KeyPath="yes" />
</RegistryKey>
<RegistryKey Root="HKLM" Key="SOFTWARE\Classes\CLSID\{2D2FBE3A-9A88-4308-A52E-7EF63CA7CF48}\InprocServer32">
<RegistryValue Type="string" Value="[INSTALLDIR]stirling_thumbnail_handler.dll" />
<RegistryValue Type="string" Name="ThreadingModel" Value="Apartment" />
</RegistryKey>
</Component>
<Component Id="ThumbnailHandlerShellexComponent" Guid="*">
<RegistryKey Root="HKLM" Key="SOFTWARE\Classes\.pdf\shellex\{E357FCCD-A995-4576-B01F-234630154E96}">
<RegistryValue Type="string" Value="{2D2FBE3A-9A88-4308-A52E-7EF63CA7CF48}" KeyPath="yes" />
</RegistryKey>
</Component>
</DirectoryRef>
<ComponentGroup Id="ProvisioningComponentGroup">
<ComponentRef Id="ProvisionerBinaryComponent" />
<ComponentRef Id="ThumbnailHandlerDllComponent" />
<ComponentRef Id="ThumbnailHandlerClsidComponent" />
<ComponentRef Id="ThumbnailHandlerShellexComponent" />
</ComponentGroup>
<CustomAction