mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2026-04-22 23:08:53 +02:00
thumbnail preview fixes windows (#6074)
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"));
|
||||
|
||||
1
frontend/src-tauri/thumbnail-handler/.gitignore
vendored
Normal file
1
frontend/src-tauri/thumbnail-handler/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/target
|
||||
174
frontend/src-tauri/thumbnail-handler/Cargo.lock
generated
Normal file
174
frontend/src-tauri/thumbnail-handler/Cargo.lock
generated
Normal 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"
|
||||
26
frontend/src-tauri/thumbnail-handler/Cargo.toml
Normal file
26
frontend/src-tauri/thumbnail-handler/Cargo.toml
Normal 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"
|
||||
61
frontend/src-tauri/thumbnail-handler/README.md
Normal file
61
frontend/src-tauri/thumbnail-handler/README.md
Normal 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.
|
||||
383
frontend/src-tauri/thumbnail-handler/src/lib.rs
Normal file
383
frontend/src-tauri/thumbnail-handler/src/lib.rs
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user