From 60c036e98066ccf2b4765e621d0038a6c18c55d7 Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com> Date: Wed, 15 Apr 2026 23:25:38 +0100 Subject: [PATCH] thumbnail preview fixes windows (#6074) --- .../software/common/util/ProcessExecutor.java | 5 +- docker/embedded/compose/test_cicd.yml | 4 - frontend/scripts/build-provisioner.mjs | 12 + .../src-tauri/thumbnail-handler/.gitignore | 1 + .../src-tauri/thumbnail-handler/Cargo.lock | 174 ++++++++ .../src-tauri/thumbnail-handler/Cargo.toml | 26 ++ .../src-tauri/thumbnail-handler/README.md | 61 +++ .../src-tauri/thumbnail-handler/src/lib.rs | 383 ++++++++++++++++++ .../src-tauri/windows/wix/provisioning.wxs | 24 ++ 9 files changed, 684 insertions(+), 6 deletions(-) create mode 100644 frontend/src-tauri/thumbnail-handler/.gitignore create mode 100644 frontend/src-tauri/thumbnail-handler/Cargo.lock create mode 100644 frontend/src-tauri/thumbnail-handler/Cargo.toml create mode 100644 frontend/src-tauri/thumbnail-handler/README.md create mode 100644 frontend/src-tauri/thumbnail-handler/src/lib.rs diff --git a/app/common/src/main/java/stirling/software/common/util/ProcessExecutor.java b/app/common/src/main/java/stirling/software/common/util/ProcessExecutor.java index 0e48628d8a..99d25838be 100644 --- a/app/common/src/main/java/stirling/software/common/util/ProcessExecutor.java +++ b/app/common/src/main/java/stirling/software/common/util/ProcessExecutor.java @@ -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(); diff --git a/docker/embedded/compose/test_cicd.yml b/docker/embedded/compose/test_cicd.yml index c6043078a0..fe165c8644 100644 --- a/docker/embedded/compose/test_cicd.yml +++ b/docker/embedded/compose/test_cicd.yml @@ -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 diff --git a/frontend/scripts/build-provisioner.mjs b/frontend/scripts/build-provisioner.mjs index 52240b766e..511e383a0a 100644 --- a/frontend/scripts/build-provisioner.mjs +++ b/frontend/scripts/build-provisioner.mjs @@ -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")); diff --git a/frontend/src-tauri/thumbnail-handler/.gitignore b/frontend/src-tauri/thumbnail-handler/.gitignore new file mode 100644 index 0000000000..ea8c4bf7f3 --- /dev/null +++ b/frontend/src-tauri/thumbnail-handler/.gitignore @@ -0,0 +1 @@ +/target diff --git a/frontend/src-tauri/thumbnail-handler/Cargo.lock b/frontend/src-tauri/thumbnail-handler/Cargo.lock new file mode 100644 index 0000000000..713a4fb9e8 --- /dev/null +++ b/frontend/src-tauri/thumbnail-handler/Cargo.lock @@ -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" diff --git a/frontend/src-tauri/thumbnail-handler/Cargo.toml b/frontend/src-tauri/thumbnail-handler/Cargo.toml new file mode 100644 index 0000000000..87b40cad19 --- /dev/null +++ b/frontend/src-tauri/thumbnail-handler/Cargo.toml @@ -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" diff --git a/frontend/src-tauri/thumbnail-handler/README.md b/frontend/src-tauri/thumbnail-handler/README.md new file mode 100644 index 0000000000..3deffaab96 --- /dev/null +++ b/frontend/src-tauri/thumbnail-handler/README.md @@ -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. diff --git a/frontend/src-tauri/thumbnail-handler/src/lib.rs b/frontend/src-tauri/thumbnail-handler/src/lib.rs new file mode 100644 index 0000000000..f75b10d030 --- /dev/null +++ b/frontend/src-tauri/thumbnail-handler/src/lib.rs @@ -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>, +} + +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 (with loop for short reads) +// --------------------------------------------------------------------------- + +fn read_istream_to_vec(stream: &IStream) -> windows::core::Result> { + 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 { + 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 { + 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::() 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 + } +} diff --git a/frontend/src-tauri/windows/wix/provisioning.wxs b/frontend/src-tauri/windows/wix/provisioning.wxs index 2d908815be..7c6801a973 100644 --- a/frontend/src-tauri/windows/wix/provisioning.wxs +++ b/frontend/src-tauri/windows/wix/provisioning.wxs @@ -13,10 +13,34 @@ + + + + + + + + + + + + + + + + + + + + + + + +