Skip to content

Commit 60c036e

Browse files
authored
thumbnail preview fixes windows (Stirling-Tools#6074)
1 parent cc5a0b8 commit 60c036e

9 files changed

Lines changed: 684 additions & 6 deletions

File tree

app/common/src/main/java/stirling/software/common/util/ProcessExecutor.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -282,8 +282,9 @@ public ProcessExecutorResult runCommandWithOutputHandling(
282282
boolean finished = process.waitFor(timeoutDuration, TimeUnit.MINUTES);
283283

284284
if (!finished) {
285-
// Terminate the process
286-
process.destroy();
285+
// Kill the entire process tree (descendants first, then the process itself)
286+
process.descendants().forEach(ProcessHandle::destroyForcibly);
287+
process.destroyForcibly();
287288
// Interrupt the reader threads
288289
errorReaderThread.interrupt();
289290
outputReaderThread.interrupt();

docker/embedded/compose/test_cicd.yml

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,6 @@ services:
55
build:
66
context: ../../../
77
dockerfile: docker/embedded/Dockerfile.fat
8-
deploy:
9-
resources:
10-
limits:
11-
memory: 4G
128
healthcheck:
139
test: ["CMD-SHELL", "curl -f -H 'X-API-KEY: 123456789' http://localhost:8080${SYSTEM_ROOTURIPATH:-''}/api/v1/info/status | grep -q 'UP'"]
1410
interval: 5s

frontend/scripts/build-provisioner.mjs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,15 @@ mkdirSync(wixDir, { recursive: true });
2222

2323
const destExe = join(wixDir, "stirling-provision.exe");
2424
copyFileSync(provisionerExe, destExe);
25+
26+
// --- Thumbnail handler DLL ---
27+
const thumbManifest = join(tauriDir, "thumbnail-handler", "Cargo.toml");
28+
29+
execFileSync("cargo", ["build", "--release", "--manifest-path", thumbManifest], { stdio: "inherit" });
30+
31+
const thumbDll = join(tauriDir, "thumbnail-handler", "target", "release", "stirling_thumbnail_handler.dll");
32+
if (!existsSync(thumbDll)) {
33+
throw new Error(`Thumbnail handler DLL not found at ${thumbDll}`);
34+
}
35+
36+
copyFileSync(thumbDll, join(wixDir, "stirling_thumbnail_handler.dll"));
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/target

frontend/src-tauri/thumbnail-handler/Cargo.lock

Lines changed: 174 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
[package]
2+
name = "stirling-thumbnail-handler"
3+
version = "0.1.0"
4+
edition = "2021"
5+
6+
[lib]
7+
crate-type = ["cdylib"]
8+
9+
[dependencies.windows]
10+
version = "0.58"
11+
features = [
12+
"implement",
13+
"Win32_Foundation",
14+
"Win32_System_Com",
15+
"Win32_System_Com_StructuredStorage",
16+
"Win32_Graphics_Gdi",
17+
"Win32_Graphics_Imaging",
18+
"Win32_UI_Shell",
19+
"Win32_UI_Shell_PropertiesSystem",
20+
"Storage_Streams",
21+
"Data_Pdf",
22+
"Foundation",
23+
]
24+
25+
[dependencies]
26+
windows-core = "0.58"
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# Windows PDF Thumbnail Handler
2+
3+
A lightweight COM DLL that provides PDF page-preview thumbnails in Windows Explorer when Stirling-PDF is the default PDF application.
4+
5+
## Why this exists
6+
7+
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.
8+
9+
## How it works
10+
11+
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.
12+
2. **Shell calls `IInitializeWithStream`** — passes the PDF file content as an `IStream`.
13+
3. **Shell calls `IThumbnailProvider::GetThumbnail(cx)`** — requests a bitmap of size `cx × cx`.
14+
4. **The DLL renders page 1** using the built-in `Windows.Data.Pdf` WinRT API (the same engine Edge uses), preserving aspect ratio.
15+
5. **WIC decodes the rendered PNG** into BGRA pixels, which are copied into an `HBITMAP` via `CreateDIBSection`.
16+
6. **Explorer displays the bitmap** as the file's thumbnail.
17+
18+
All COM methods are wrapped in `catch_unwind` so a malformed PDF cannot crash Explorer.
19+
20+
## Technical details
21+
22+
| | |
23+
|---|---|
24+
| **Language** | Rust (cdylib) |
25+
| **DLL size** | ~156 KB |
26+
| **External deps** | None — uses only Windows built-in APIs |
27+
| **PDF renderer** | `Windows.Data.Pdf` (WinRT, Windows 10+) |
28+
| **Image decode** | WIC (`IWICImagingFactory`) with BGRA32 format conversion |
29+
| **COM CLSID** | `{2D2FBE3A-9A88-4308-A52E-7EF63CA7CF48}` |
30+
| **Threading model** | Apartment (STA — standard for shell extensions) |
31+
| **Min Windows** | Windows 10 |
32+
33+
## Registry entries (managed by MSI)
34+
35+
The WiX installer (`provisioning.wxs`) registers:
36+
37+
- **CLSID** at `HKLM\SOFTWARE\Classes\CLSID\{2D2FBE3A-...}\InprocServer32` pointing to the DLL
38+
- **Shellex** at `HKLM\SOFTWARE\Classes\.pdf\shellex\{E357FCCD-...}` linking `.pdf` thumbnails to our CLSID
39+
40+
Both are automatically removed on uninstall.
41+
42+
## Building
43+
44+
The DLL is built automatically as part of the Tauri build pipeline via `build-provisioner.mjs`:
45+
46+
```bash
47+
cd frontend
48+
npm run tauri-build
49+
```
50+
51+
To build the DLL standalone:
52+
53+
```bash
54+
cd frontend/src-tauri/thumbnail-handler
55+
cargo build --release
56+
# Output: target/release/stirling_thumbnail_handler.dll
57+
```
58+
59+
## Linux / macOS
60+
61+
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.

0 commit comments

Comments
 (0)