forked from 1b-agent/opencadstudio-upstream
286 lines
9.7 KiB
Rust
286 lines
9.7 KiB
Rust
//! Windows Explorer thumbnail handler for DWG files.
|
|
//!
|
|
//! Implements `IThumbnailProvider` (+ `IInitializeWithFile`) as a COM in-proc
|
|
//! server. Explorer instantiates it for `.dwg` files, hands it the path, then
|
|
//! calls `GetThumbnail`, which extracts the DWG's embedded preview via the
|
|
//! shared [`dwg_thumbnailer`] core and returns it as an HBITMAP.
|
|
//!
|
|
//! ## Build & register (on Windows, elevated)
|
|
//! ```text
|
|
//! cargo build -p dwg-thumbnailer-win --release
|
|
//! regsvr32 dwg_thumbnailer_win.dll :: register
|
|
//! regsvr32 /u dwg_thumbnailer_win.dll :: unregister
|
|
//! ```
|
|
//! Then restart Explorer (or run `ie4uinit.exe -show`) to refresh thumbnails.
|
|
//!
|
|
//! NOTE: this module is `cfg(windows)`-only and was authored on a Linux host,
|
|
//! so it has NOT been compiled or tested. Build and test it on Windows; minor
|
|
//! fixups may be needed for your exact `windows` crate version.
|
|
|
|
#![cfg(windows)]
|
|
|
|
use std::cell::RefCell;
|
|
use std::ffi::c_void;
|
|
|
|
use windows::core::{implement, IUnknown, Interface, GUID, HRESULT, PCWSTR};
|
|
use windows::Win32::Foundation::{
|
|
CLASS_E_CLASSNOTAVAILABLE, CLASS_E_NOAGGREGATION, E_FAIL, E_INVALIDARG, HMODULE, S_OK,
|
|
WIN32_ERROR,
|
|
};
|
|
use windows::Win32::Graphics::Gdi::{
|
|
CreateDIBSection, BITMAPINFO, BITMAPINFOHEADER, BI_RGB, DIB_RGB_COLORS, HBITMAP, HDC,
|
|
};
|
|
use windows::Win32::System::Com::{IClassFactory, IClassFactory_Impl};
|
|
use windows::Win32::System::LibraryLoader::GetModuleFileNameW;
|
|
use windows::Win32::System::Registry::{
|
|
RegCloseKey, RegCreateKeyExW, RegDeleteTreeW, RegSetValueExW, HKEY, HKEY_CLASSES_ROOT,
|
|
KEY_WRITE, REG_OPTION_NON_VOLATILE, REG_SZ,
|
|
};
|
|
use windows::Win32::System::SystemServices::{DLL_PROCESS_ATTACH, DLL_PROCESS_DETACH};
|
|
use windows::Win32::UI::Shell::PropertiesSystem::{IInitializeWithFile, IInitializeWithFile_Impl};
|
|
use windows::Win32::UI::Shell::{
|
|
IThumbnailProvider, IThumbnailProvider_Impl, SHChangeNotify, SHCNE_ASSOCCHANGED, SHCNF_IDLIST,
|
|
WTSAT_ARGB, WTS_ALPHATYPE,
|
|
};
|
|
|
|
/// CLSID of this thumbnail provider (stable; used in the registry keys).
|
|
/// {8F2A9C41-3B6E-4E2D-9C7A-1E0B5D6F42AA}
|
|
const CLSID_DWG_THUMB: GUID = GUID::from_u128(0x8F2A9C41_3B6E_4E2D_9C7A_1E0B5D6F42AA);
|
|
/// The interface id Explorer looks up under `.dwg\ShellEx`.
|
|
const IID_ITHUMBNAILPROVIDER: &str = "{e357fccd-a995-4576-b01f-234630154e96}";
|
|
|
|
/// Our own module handle, captured in `DllMain` — needed to write the DLL path
|
|
/// into `InprocServer32` during registration.
|
|
static mut SELF_HMODULE: HMODULE = HMODULE(std::ptr::null_mut());
|
|
|
|
#[no_mangle]
|
|
extern "system" fn DllMain(hinst: HMODULE, reason: u32, _reserved: *mut c_void) -> bool {
|
|
if reason == DLL_PROCESS_ATTACH {
|
|
unsafe { SELF_HMODULE = hinst };
|
|
} else if reason == DLL_PROCESS_DETACH {
|
|
}
|
|
true
|
|
}
|
|
|
|
// ── The provider COM object ──────────────────────────────────────────────────
|
|
|
|
#[implement(IThumbnailProvider, IInitializeWithFile)]
|
|
#[derive(Default)]
|
|
struct DwgThumbProvider {
|
|
path: RefCell<Option<String>>,
|
|
}
|
|
|
|
impl IInitializeWithFile_Impl for DwgThumbProvider_Impl {
|
|
fn Initialize(&self, pszfilepath: &PCWSTR, _grfmode: u32) -> windows::core::Result<()> {
|
|
let path = unsafe { pszfilepath.to_string() }
|
|
.map_err(|_| windows::core::Error::from(E_INVALIDARG))?;
|
|
*self.path.borrow_mut() = Some(path);
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
impl IThumbnailProvider_Impl for DwgThumbProvider_Impl {
|
|
fn GetThumbnail(
|
|
&self,
|
|
cx: u32,
|
|
phbmp: *mut HBITMAP,
|
|
pdwalpha: *mut WTS_ALPHATYPE,
|
|
) -> windows::core::Result<()> {
|
|
let path = self
|
|
.path
|
|
.borrow()
|
|
.clone()
|
|
.ok_or(windows::core::Error::from(E_FAIL))?;
|
|
let mut img = dwg_thumbnailer::extract(std::path::Path::new(&path), cx.max(1))
|
|
.ok_or(windows::core::Error::from(E_FAIL))?;
|
|
dwg_thumbnailer::badge_dwg(&mut img); // full-width "DWG" band
|
|
let hbmp = unsafe { rgba_to_hbitmap(&img)? };
|
|
unsafe {
|
|
*phbmp = hbmp;
|
|
*pdwalpha = WTSAT_ARGB;
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
/// Build a 32-bit top-down BGRA `HBITMAP` from an RGBA image.
|
|
unsafe fn rgba_to_hbitmap(img: &dwg_thumbnailer::RgbaImage) -> windows::core::Result<HBITMAP> {
|
|
let (w, h) = (img.width() as i32, img.height() as i32);
|
|
let bi = BITMAPINFO {
|
|
bmiHeader: BITMAPINFOHEADER {
|
|
biSize: std::mem::size_of::<BITMAPINFOHEADER>() as u32,
|
|
biWidth: w,
|
|
biHeight: -h, // negative → top-down rows
|
|
biPlanes: 1,
|
|
biBitCount: 32,
|
|
biCompression: BI_RGB.0,
|
|
..Default::default()
|
|
},
|
|
..Default::default()
|
|
};
|
|
let mut bits: *mut c_void = std::ptr::null_mut();
|
|
let hbmp = CreateDIBSection(HDC::default(), &bi, DIB_RGB_COLORS, &mut bits, None, 0)?;
|
|
if bits.is_null() {
|
|
return Err(E_FAIL.into());
|
|
}
|
|
let dst = std::slice::from_raw_parts_mut(bits as *mut u8, (w * h * 4) as usize);
|
|
for (i, px) in img.pixels().enumerate() {
|
|
let [r, g, b, a] = px.0;
|
|
dst[i * 4] = b;
|
|
dst[i * 4 + 1] = g;
|
|
dst[i * 4 + 2] = r;
|
|
dst[i * 4 + 3] = a;
|
|
}
|
|
Ok(hbmp)
|
|
}
|
|
|
|
// ── Class factory ────────────────────────────────────────────────────────────
|
|
|
|
#[implement(IClassFactory)]
|
|
struct Factory;
|
|
|
|
impl IClassFactory_Impl for Factory_Impl {
|
|
fn CreateInstance(
|
|
&self,
|
|
punkouter: Option<&IUnknown>,
|
|
riid: *const GUID,
|
|
ppvobject: *mut *mut c_void,
|
|
) -> windows::core::Result<()> {
|
|
if punkouter.is_some() {
|
|
return Err(CLASS_E_NOAGGREGATION.into());
|
|
}
|
|
let provider: IUnknown = DwgThumbProvider::default().into();
|
|
unsafe { provider.query(&*riid, ppvobject).ok() }
|
|
}
|
|
|
|
fn LockServer(&self, _flock: windows::Win32::Foundation::BOOL) -> windows::core::Result<()> {
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
// ── DLL exports ──────────────────────────────────────────────────────────────
|
|
|
|
#[no_mangle]
|
|
extern "system" fn DllGetClassObject(
|
|
rclsid: *const GUID,
|
|
riid: *const GUID,
|
|
ppv: *mut *mut c_void,
|
|
) -> HRESULT {
|
|
unsafe {
|
|
if *rclsid != CLSID_DWG_THUMB {
|
|
return CLASS_E_CLASSNOTAVAILABLE;
|
|
}
|
|
let factory: IClassFactory = Factory.into();
|
|
factory.query(&*riid, ppv)
|
|
}
|
|
}
|
|
|
|
#[no_mangle]
|
|
extern "system" fn DllCanUnloadNow() -> HRESULT {
|
|
// Simplification: report unloadable only when COM has released everything.
|
|
// A conservative always-`S_FALSE` keeps the DLL resident (safe, if leakier).
|
|
windows::Win32::Foundation::S_FALSE
|
|
}
|
|
|
|
#[no_mangle]
|
|
extern "system" fn DllRegisterServer() -> HRESULT {
|
|
match register(true) {
|
|
Ok(()) => S_OK,
|
|
Err(e) => e.code(),
|
|
}
|
|
}
|
|
|
|
#[no_mangle]
|
|
extern "system" fn DllUnregisterServer() -> HRESULT {
|
|
match register(false) {
|
|
Ok(()) => S_OK,
|
|
Err(e) => e.code(),
|
|
}
|
|
}
|
|
|
|
// ── Registration (HKCR) ──────────────────────────────────────────────────────
|
|
|
|
fn module_path() -> windows::core::Result<String> {
|
|
let mut buf = [0u16; 1024];
|
|
let len = unsafe { GetModuleFileNameW(SELF_HMODULE, &mut buf) };
|
|
if len == 0 {
|
|
return Err(E_FAIL.into());
|
|
}
|
|
Ok(String::from_utf16_lossy(&buf[..len as usize]))
|
|
}
|
|
|
|
fn register(install: bool) -> windows::core::Result<()> {
|
|
let clsid = format!("{{{:?}}}", CLSID_DWG_THUMB); // "{8F2A9C41-...}"
|
|
let clsid_key = format!("CLSID\\{clsid}");
|
|
let inproc_key = format!("{clsid_key}\\InprocServer32");
|
|
let dwg_shellex = format!(".dwg\\ShellEx\\{IID_ITHUMBNAILPROVIDER}");
|
|
|
|
if install {
|
|
let dll = module_path()?;
|
|
set_value(&clsid_key, None, "OpenCADStudio DWG Thumbnail Provider")?;
|
|
set_value(&inproc_key, None, &dll)?;
|
|
set_value(&inproc_key, Some("ThreadingModel"), "Apartment")?;
|
|
set_value(&dwg_shellex, None, &clsid)?;
|
|
} else {
|
|
let _ = delete_tree(&clsid_key);
|
|
let _ = delete_tree(&dwg_shellex);
|
|
}
|
|
unsafe { SHChangeNotify(SHCNE_ASSOCCHANGED, SHCNF_IDLIST, None, None) };
|
|
Ok(())
|
|
}
|
|
|
|
fn wide(s: &str) -> Vec<u16> {
|
|
s.encode_utf16().chain(std::iter::once(0)).collect()
|
|
}
|
|
|
|
fn set_value(sub: &str, name: Option<&str>, value: &str) -> windows::core::Result<()> {
|
|
let sub_w = wide(sub);
|
|
let mut hkey = HKEY::default();
|
|
let rc = unsafe {
|
|
RegCreateKeyExW(
|
|
HKEY_CLASSES_ROOT,
|
|
PCWSTR(sub_w.as_ptr()),
|
|
0,
|
|
PCWSTR::null(),
|
|
REG_OPTION_NON_VOLATILE,
|
|
KEY_WRITE,
|
|
None,
|
|
&mut hkey,
|
|
None,
|
|
)
|
|
};
|
|
if rc != WIN32_ERROR(0) {
|
|
return Err(E_FAIL.into());
|
|
}
|
|
let val_w = wide(value);
|
|
let bytes = unsafe { std::slice::from_raw_parts(val_w.as_ptr() as *const u8, val_w.len() * 2) };
|
|
let name_w = name.map(wide);
|
|
let rc = unsafe {
|
|
RegSetValueExW(
|
|
hkey,
|
|
name_w
|
|
.as_ref()
|
|
.map_or(PCWSTR::null(), |n| PCWSTR(n.as_ptr())),
|
|
0,
|
|
REG_SZ,
|
|
Some(bytes),
|
|
)
|
|
};
|
|
unsafe {
|
|
let _ = RegCloseKey(hkey);
|
|
}
|
|
if rc != WIN32_ERROR(0) {
|
|
return Err(E_FAIL.into());
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn delete_tree(sub: &str) -> windows::core::Result<()> {
|
|
let sub_w = wide(sub);
|
|
let rc = unsafe { RegDeleteTreeW(HKEY_CLASSES_ROOT, PCWSTR(sub_w.as_ptr())) };
|
|
if rc != WIN32_ERROR(0) {
|
|
return Err(E_FAIL.into());
|
|
}
|
|
Ok(())
|
|
}
|