From ba3b3202e882de4a325c24a9598f57e4b011f8a7 Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Sat, 11 Jul 2026 01:27:17 +0300 Subject: [PATCH] feat(thumbnails): embed DWG previews and wire cross-platform file thumbnails MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Round-trip a raster preview through the DWG's embedded preview slot and surface it to the OS file managers, so drawings show their contents in Explorer / Finder / Nautilus instead of a generic document icon. OCS: * io::thumbnail — rasterize the scene to a BMP DIB on save (embedded via acadrust's new Preview type) and read it back on open for the Start page recent list; `--dwg-thumbnail IN OUT SIZE` CLI extracts a badged PNG for external thumbnailers. * io::file_association::install_thumbnailer — self-install the OS integration on startup: Linux writes a .thumbnailer + hicolor mimetype icons; Windows registers the IThumbnailProvider DLL under HKCU. Shared core: * crates/dwg-thumbnailer — lean (image-only) preview extractor + the `badge_dwg` full-width "DWG" band, used by every platform so the ribbon is single-sourced. Ships an rlib+staticlib. * crates/dwg-thumbnailer-win — IThumbnailProvider COM in-proc server (cfg(windows), CI-built as a workspace member). * macos/ — QuickLook thumbnail extension (Swift + C-ABI bridge to the core staticlib), assembled into a .appex. Icons & packaging (single SVG source -> per-platform assets in CI): * assets/mimetypes/image-vnd.{dwg,dxf}.svg — themed file icons. * packaging: WiX ships dwg/dxf.ico + the thumbnail DLL and points the ProgIds at them; Info.plist gets CFBundleTypeIconFile + the QuickLook extension in Contents/PlugIns. * release.yml — generate .ico (Windows) and .icns (macOS) from the SVGs, build the QuickLook .appex, and bundle everything. Co-Authored-By: Claude Opus 4.8 --- .github/workflows/release.yml | 59 +++- Cargo.lock | 16 + Cargo.toml | 12 +- assets/mimetypes/image-vnd.dwg.svg | 15 + assets/mimetypes/image-vnd.dxf.svg | 15 + crates/dwg-thumbnailer-win/Cargo.toml | 26 ++ crates/dwg-thumbnailer-win/src/lib.rs | 279 ++++++++++++++++++ crates/dwg-thumbnailer/Cargo.toml | 19 ++ crates/dwg-thumbnailer/README.md | 80 +++++ crates/dwg-thumbnailer/assets/dwg-label.png | Bin 0 -> 4369 bytes crates/dwg-thumbnailer/macos/Info.plist | 42 +++ .../macos/ThumbnailProvider.swift | 49 +++ .../dwg-thumbnailer/macos/dwg_thumbnailer.h | 18 ++ crates/dwg-thumbnailer/src/lib.rs | 247 ++++++++++++++++ packaging/Info.plist | 4 + packaging/windows/main.wxs | 18 +- src/cli.rs | 5 + src/io/file_association.rs | 124 ++++++++ src/io/thumbnail.rs | 113 +++---- src/main.rs | 20 ++ 20 files changed, 1085 insertions(+), 76 deletions(-) create mode 100644 assets/mimetypes/image-vnd.dwg.svg create mode 100644 assets/mimetypes/image-vnd.dxf.svg create mode 100644 crates/dwg-thumbnailer-win/Cargo.toml create mode 100644 crates/dwg-thumbnailer-win/src/lib.rs create mode 100644 crates/dwg-thumbnailer/Cargo.toml create mode 100644 crates/dwg-thumbnailer/README.md create mode 100644 crates/dwg-thumbnailer/assets/dwg-label.png create mode 100644 crates/dwg-thumbnailer/macos/Info.plist create mode 100644 crates/dwg-thumbnailer/macos/ThumbnailProvider.swift create mode 100644 crates/dwg-thumbnailer/macos/dwg_thumbnailer.h create mode 100644 crates/dwg-thumbnailer/src/lib.rs diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 86ce62b47..50c88047f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -93,6 +93,17 @@ jobs: -define icon:auto-resize=16,24,32,48,64,128,256 ` packaging/windows/AppIcon.ico + - name: Convert DWG/DXF file icons to ICO + shell: pwsh + run: | + # DWG/DXF Explorer file icons, from the single-source mimetype SVGs. + magick assets/mimetypes/image-vnd.dwg.svg ` + -define icon:auto-resize=16,24,32,48,64,128,256 ` + packaging/windows/dwg.ico + magick assets/mimetypes/image-vnd.dxf.svg ` + -define icon:auto-resize=16,24,32,48,64,128,256 ` + packaging/windows/dxf.ico + - name: Build env: OCS_PATREON_TOKEN: ${{ secrets.OCS_PATREON_TOKEN }} @@ -237,14 +248,60 @@ jobs: done iconutil -c icns OpenCADStudio.iconset -o AppIcon.icns + - name: Build DWG/DXF document .icns from the same SVG sources + run: | + # Finder document icons (Info.plist CFBundleTypeIconFile = DWG/DXF), + # generated from the single-source mimetype SVGs — never hand-drawn. + for T in dwg dxf; do + mkdir -p "$T.iconset" + for SIZE in 16 32 64 128 256 512 1024; do + rsvg-convert -w $SIZE -h $SIZE "assets/mimetypes/image-vnd.$T.svg" \ + -o "$T.iconset/icon_${SIZE}x${SIZE}.png" + done + for BASE in 16 32 128 256 512; do + cp "$T.iconset/icon_$((BASE*2))x$((BASE*2)).png" \ + "$T.iconset/icon_${BASE}x${BASE}@2x.png" + done + iconutil -c icns "$T.iconset" -o "$(echo "$T" | tr a-z A-Z).icns" + done + + - name: Build DWG QuickLook thumbnail extension (.appex) + run: | + # A QuickLook thumbnail App Extension, built without an Xcode project: + # swiftc compiles the provider, links the Rust core static lib (its C + # ABI) + the system frameworks, and we hand-assemble the .appex bundle. + # `cargo build` already produced libdwg_thumbnailer.a (workspace member, + # crate-type staticlib). The whole app is ad-hoc signed below, which + # deep-signs the embedded extension too. + set -e + VERSION="${{ github.ref_name }}"; VERSION="${VERSION#v}"; [ -z "$VERSION" ] && VERSION="0.0.0" + EXT=DWGThumbnail.appex + rm -rf "$EXT"; mkdir -p "$EXT/Contents/MacOS" + swiftc \ + -sdk "$(xcrun --sdk macosx --show-sdk-path)" \ + -target arm64-apple-macos11 \ + -O -parse-as-library -application-extension \ + -module-name DWGThumbnail \ + -import-objc-header crates/dwg-thumbnailer/macos/dwg_thumbnailer.h \ + crates/dwg-thumbnailer/macos/ThumbnailProvider.swift \ + -L target/aarch64-apple-darwin/release -ldwg_thumbnailer \ + -framework QuickLookThumbnailing -framework CoreGraphics \ + -framework ImageIO -framework Foundation \ + -Xlinker -e -Xlinker _NSExtensionMain \ + -o "$EXT/Contents/MacOS/DWGThumbnail" + sed "s/__VERSION__/$VERSION/g" crates/dwg-thumbnailer/macos/Info.plist > "$EXT/Contents/Info.plist" + - name: Assemble .app bundle run: | APP=OpenCADStudio.app rm -rf "$APP" - mkdir -p "$APP/Contents/MacOS" "$APP/Contents/Resources" + mkdir -p "$APP/Contents/MacOS" "$APP/Contents/Resources" "$APP/Contents/PlugIns" cp target/aarch64-apple-darwin/release/OpenCADStudio "$APP/Contents/MacOS/OpenCADStudio" chmod +x "$APP/Contents/MacOS/OpenCADStudio" cp AppIcon.icns "$APP/Contents/Resources/AppIcon.icns" + cp DWG.icns DXF.icns "$APP/Contents/Resources/" + # QuickLook thumbnail extension. + cp -R DWGThumbnail.appex "$APP/Contents/PlugIns/" # Substitute version into Info.plist. VERSION="${{ github.ref_name }}" VERSION="${VERSION#v}" diff --git a/Cargo.lock b/Cargo.lock index 7f80eda2a..9fc3736c3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -21,6 +21,7 @@ dependencies = [ "clap", "console_error_panic_hook", "cosmic-text", + "dwg-thumbnailer", "env_logger", "flate2", "fontdb", @@ -1414,6 +1415,21 @@ version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "edf234dd1594d6dd434a8fb8cada51ddbbc593e40e4a01556a0b31c62da2775b" +[[package]] +name = "dwg-thumbnailer" +version = "0.1.0" +dependencies = [ + "image", +] + +[[package]] +name = "dwg-thumbnailer-win" +version = "0.1.0" +dependencies = [ + "dwg-thumbnailer", + "windows 0.58.0", +] + [[package]] name = "ecb" version = "0.1.2" diff --git a/Cargo.toml b/Cargo.toml index 57e16802e..a0071cfb7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,7 +5,15 @@ edition = "2021" build = "build.rs" [workspace] -members = ["crates/ocs_plugin_api"] +# `dwg-thumbnailer-win` is a Windows-only COM DLL (`#![cfg(windows)]`): it +# compiles to an empty cdylib on Linux/macOS and to the real handler on Windows, +# where the `build-windows` CI job (`cargo build --release`) compiles and thus +# verifies it. +members = [ + "crates/ocs_plugin_api", + "crates/dwg-thumbnailer", + "crates/dwg-thumbnailer-win", +] # Embed the application icon into the Windows .exe so Explorer, the taskbar, # the Start-menu tile and file-association entries show it (issue #107). @@ -50,6 +58,8 @@ clap = { version = "4", features = ["derive"] } # Opt-in logging via --log / RUST_LOG (surfaces wgpu / iced / winit diagnostics). env_logger = "0.11" acadrust = "0.4" +# Shared DWG embedded-preview extraction (Start-page + file-manager thumbnails). +dwg-thumbnailer = { path = "crates/dwg-thumbnailer" } flate2 = "1" image = { version = "0.25", default-features = false, features = ["png", "jpeg", "bmp", "tiff"] } inventory = "0.3" diff --git a/assets/mimetypes/image-vnd.dwg.svg b/assets/mimetypes/image-vnd.dwg.svg new file mode 100644 index 000000000..64f4f704e --- /dev/null +++ b/assets/mimetypes/image-vnd.dwg.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + DWG + diff --git a/assets/mimetypes/image-vnd.dxf.svg b/assets/mimetypes/image-vnd.dxf.svg new file mode 100644 index 000000000..988f88311 --- /dev/null +++ b/assets/mimetypes/image-vnd.dxf.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + DXF + diff --git a/crates/dwg-thumbnailer-win/Cargo.toml b/crates/dwg-thumbnailer-win/Cargo.toml new file mode 100644 index 000000000..9750103e5 --- /dev/null +++ b/crates/dwg-thumbnailer-win/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "dwg-thumbnailer-win" +version = "0.1.0" +edition = "2021" +description = "Windows Explorer thumbnail handler (IThumbnailProvider) for DWG files" +license = "MIT OR Apache-2.0" + +# A COM in-proc server DLL. Off Windows this compiles to an empty cdylib so the +# workspace still builds on Linux/macOS; the real handler is cfg(windows) only. +[lib] +crate-type = ["cdylib"] + +[dependencies] +dwg_thumbnailer = { package = "dwg-thumbnailer", path = "../dwg-thumbnailer" } + +[target.'cfg(windows)'.dependencies] +windows = { version = "0.58", features = [ + "implement", + "Win32_Foundation", + "Win32_System_Com", + "Win32_System_Com_StructuredStorage", + "Win32_System_Registry", + "Win32_UI_Shell", + "Win32_UI_Shell_PropertiesSystem", + "Win32_Graphics_Gdi", +] } diff --git a/crates/dwg-thumbnailer-win/src/lib.rs b/crates/dwg-thumbnailer-win/src/lib.rs new file mode 100644 index 000000000..07f8310d7 --- /dev/null +++ b/crates/dwg-thumbnailer-win/src/lib.rs @@ -0,0 +1,279 @@ +//! 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, + WTS_ALPHATYPE, WTSAT_ARGB, +}; + +/// 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>, +} + +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: &image::RgbaImage) -> windows::core::Result { + let (w, h) = (img.width() as i32, img.height() as i32); + let bi = BITMAPINFO { + bmiHeader: BITMAPINFOHEADER { + biSize: std::mem::size_of::() 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::core::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 { + 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 { + 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(()) +} diff --git a/crates/dwg-thumbnailer/Cargo.toml b/crates/dwg-thumbnailer/Cargo.toml new file mode 100644 index 000000000..d47191b60 --- /dev/null +++ b/crates/dwg-thumbnailer/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "dwg-thumbnailer" +version = "0.1.0" +edition = "2021" +description = "Extracts the embedded preview image from a DWG file for OS file-manager thumbnails" +license = "MIT OR Apache-2.0" + +# Shared extraction core. Consumers: OpenCADStudio (Linux, via its own +# `--dwg-thumbnail` mode), the Windows `IThumbnailProvider` DLL, and the macOS +# QuickLook extension. +# rlib — OCS and the Windows DLL call `extract` directly. +# staticlib — the macOS QuickLook extension links the C ABI (`dwg_thumbnail_png`). +[lib] +name = "dwg_thumbnailer" +path = "src/lib.rs" +crate-type = ["rlib", "staticlib"] + +[dependencies] +image = { version = "0.25", default-features = false, features = ["png", "bmp"] } diff --git a/crates/dwg-thumbnailer/README.md b/crates/dwg-thumbnailer/README.md new file mode 100644 index 000000000..ca61acd12 --- /dev/null +++ b/crates/dwg-thumbnailer/README.md @@ -0,0 +1,80 @@ +# DWG thumbnails for OS file managers + +`dwg_thumbnailer::extract` reads **only** the embedded preview from a DWG (file +header → preview seeker → parse — never the whole drawing) and returns it as an +`image::RgbaImage`. It's the shared core behind every platform's thumbnail +integration. + +``` +crates/ + dwg-thumbnailer/ this crate — shared core + macOS extension sources + dwg-thumbnailer-win/ Windows IThumbnailProvider COM DLL (depends on the core) +``` + +--- + +## Linux (COSMIC, GNOME, Nautilus, Nemo, …) — handled by OpenCADStudio itself + +No separate binary. OpenCADStudio embeds this core and, on startup, installs a +freedesktop `.thumbnailer` pointing at its own hidden `--dwg-thumbnail` mode +(see `src/io/file_association.rs::install_thumbnailer`). Launch OCS once and file +managers render DWG thumbnails; clear stale "no thumbnail" cache if needed: + +```sh +rm -f ~/.cache/thumbnails/fail/*/*.png +``` + +--- + +## Windows (Explorer) — authored, build & test on Windows + +A COM in-proc server implementing `IThumbnailProvider`. **Not compiled/tested on +the Linux dev host** — build and verify on Windows. + +```bat +cd crates\dwg-thumbnailer-win && cargo build --release +regsvr32 target\release\dwg_thumbnailer_win.dll :: register (elevated) +regsvr32 /u target\release\dwg_thumbnailer_win.dll :: unregister +ie4uinit.exe -show :: refresh thumbnails +``` + +If `regsvr32`'s self-registration needs adjusting, the equivalent registry keys +are (replace the path): + +```reg +Windows Registry Editor Version 5.00 +[HKEY_CLASSES_ROOT\CLSID\{8F2A9C41-3B6E-4E2D-9C7A-1E0B5D6F42AA}] +@="OpenCADStudio DWG Thumbnail Provider" +[HKEY_CLASSES_ROOT\CLSID\{8F2A9C41-3B6E-4E2D-9C7A-1E0B5D6F42AA}\InprocServer32] +@="C:\\path\\to\\dwg_thumbnailer_win.dll" +"ThreadingModel"="Apartment" +[HKEY_CLASSES_ROOT\.dwg\ShellEx\{e357fccd-a995-4576-b01f-234630154e96}] +@="{8F2A9C41-3B6E-4E2D-9C7A-1E0B5D6F42AA}" +``` + +--- + +## macOS (Finder) — sources provided, build in Xcode + +A QuickLook **Thumbnail Extension** (`macos/ThumbnailProvider.swift`) calls the +Rust core's C ABI, linked as a static library. Requires Xcode + a host app. + +1. Build the core as a static lib for your arch(s): + ```sh + cargo build -p dwg-thumbnailer --release --target aarch64-apple-darwin + # → target/aarch64-apple-darwin/release/libdwg_thumbnailer.a + ``` +2. In Xcode, add a **Thumbnail Extension** target to a host app. + - Use `macos/Info.plist` (lists the `com.autodesk.dwg` UTI). + - Add `macos/ThumbnailProvider.swift`. + - Add a bridging header that `#include`s `macos/dwg_thumbnailer.h`. + - Link `libdwg_thumbnailer.a` (+ system frameworks it needs). +3. Sign, install the host app, and Finder picks up the extension. + +C ABI (see `dwg_thumbnailer.h`): +`dwg_thumbnail_png(path, max_dim, &ptr, &len)` / `dwg_thumbnail_free(ptr, len)`. + +--- + +Formats: DWG BMP-DIB and PNG embedded previews decode. WMF previews and files +without a preview (or DXF) produce no thumbnail — the file manager falls back. diff --git a/crates/dwg-thumbnailer/assets/dwg-label.png b/crates/dwg-thumbnailer/assets/dwg-label.png new file mode 100644 index 0000000000000000000000000000000000000000..b479a5ad492882d80c8ed2b152bf1bd04dd8453d GIT binary patch literal 4369 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 LNkvXXu0mjfEFD{< literal 0 HcmV?d00001 diff --git a/crates/dwg-thumbnailer/macos/Info.plist b/crates/dwg-thumbnailer/macos/Info.plist new file mode 100644 index 000000000..43b03fe66 --- /dev/null +++ b/crates/dwg-thumbnailer/macos/Info.plist @@ -0,0 +1,42 @@ + + + + + + CFBundleDevelopmentRegion + en + CFBundleDisplayName + DWG Thumbnails + CFBundleExecutable + DWGThumbnail + + CFBundleIdentifier + io.github.HakanSeven12.OpenCadStudio.DWGThumbnail + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + DWGThumbnail + CFBundlePackageType + XPC! + CFBundleShortVersionString + __VERSION__ + CFBundleVersion + __VERSION__ + NSExtension + + NSExtensionPointIdentifier + com.apple.quicklook.thumbnail + NSExtensionPrincipalClass + DWGThumbnail.ThumbnailProvider + NSExtensionAttributes + + QLSupportedContentTypes + + com.autodesk.dwg + + QLThumbnailMinimumDimension + 0 + + + + diff --git a/crates/dwg-thumbnailer/macos/ThumbnailProvider.swift b/crates/dwg-thumbnailer/macos/ThumbnailProvider.swift new file mode 100644 index 000000000..62065560c --- /dev/null +++ b/crates/dwg-thumbnailer/macos/ThumbnailProvider.swift @@ -0,0 +1,49 @@ +// macOS QuickLook thumbnail extension for DWG files. +// +// Calls the Rust core (`dwg_thumbnail_png`, linked from libdwg_thumbnailer.a) +// to extract the embedded preview as PNG, then hands it to QuickLook. +// +// Add this file to a "Thumbnail Extension" target, expose the C ABI through a +// bridging header that #includes `dwg_thumbnailer.h`, and link the static lib +// (see README). QLSupportedContentTypes must list the DWG UTI (com.autodesk.dwg). + +import QuickLookThumbnailing +import CoreGraphics +import ImageIO +import Foundation + +final class ThumbnailProvider: QLThumbnailProvider { + override func provideThumbnail( + for request: QLFileThumbnailRequest, + _ handler: @escaping (QLThumbnailReply?, Error?) -> Void + ) { + let maxDim = UInt32(max(request.maximumSize.width, request.maximumSize.height)) + + var ptr: UnsafeMutablePointer? = nil + var len: Int = 0 + let ok = request.fileURL.path.withCString { cpath in + dwg_thumbnail_png(cpath, maxDim, &ptr, &len) + } + guard ok, let p = ptr, len > 0 else { + handler(nil, nil) // no preview → QuickLook falls back + return + } + let data = Data(bytes: p, count: len) + dwg_thumbnail_free(p, len) + + guard + let src = CGImageSourceCreateWithData(data as CFData, nil), + let cg = CGImageSourceCreateImageAtIndex(src, 0, nil) + else { + handler(nil, nil) + return + } + + let size = CGSize(width: cg.width, height: cg.height) + let reply = QLThumbnailReply(contextSize: size) { (ctx: CGContext) -> Bool in + ctx.draw(cg, in: CGRect(origin: .zero, size: size)) + return true + } + handler(reply, nil) + } +} diff --git a/crates/dwg-thumbnailer/macos/dwg_thumbnailer.h b/crates/dwg-thumbnailer/macos/dwg_thumbnailer.h new file mode 100644 index 000000000..144ff5dca --- /dev/null +++ b/crates/dwg-thumbnailer/macos/dwg_thumbnailer.h @@ -0,0 +1,18 @@ +/* Bridging header for the macOS QuickLook extension. + * Exposes the Rust core's C ABI (built as `libdwg_thumbnailer.a`). */ +#ifndef DWG_THUMBNAILER_H +#define DWG_THUMBNAILER_H + +#include +#include +#include + +/* Extract a DWG preview, encoded as PNG. On success writes a malloc'd buffer to + * *out_ptr / *out_len (free with dwg_thumbnail_free) and returns true. */ +bool dwg_thumbnail_png(const char *path_utf8, uint32_t max_dim, + uint8_t **out_ptr, size_t *out_len); + +/* Free a buffer returned by dwg_thumbnail_png. */ +void dwg_thumbnail_free(uint8_t *ptr, size_t len); + +#endif /* DWG_THUMBNAILER_H */ diff --git a/crates/dwg-thumbnailer/src/lib.rs b/crates/dwg-thumbnailer/src/lib.rs new file mode 100644 index 000000000..3d8f27fbf --- /dev/null +++ b/crates/dwg-thumbnailer/src/lib.rs @@ -0,0 +1,247 @@ +//! Extract the embedded preview image from a DWG file, for OS file-manager +//! thumbnails. +//! +//! Every DWG version stores an (uncompressed) preview at the raw file offset +//! recorded in the file header's preview seeker (byte `0x0D`). This crate reads +//! *only* that — no full document parse — and decodes it to an RGBA image. The +//! preview container is a fixed byte format, so this crate depends only on +//! `image` (no CAD library). Shared by OpenCADStudio (Linux, via its +//! `--dwg-thumbnail` mode), the Windows `IThumbnailProvider`, and the macOS +//! QuickLook extension. + +use std::path::Path; + +use image::{ImageFormat, RgbaImage}; + +/// Read the DWG at `path`, extract its embedded preview, and scale it so the +/// longest edge is at most `max_dim` pixels (aspect preserved). Returns `None` +/// for a DXF/other file, a missing or empty preview, or a preview in a format +/// this crate can't decode (WMF). +pub fn extract(path: &Path, max_dim: u32) -> Option { + let (format, data) = read_preview(path)?; + let img = decode(format, &data)?; + Some(downscale(img, max_dim.max(1))) +} + +/// White "DWG" wordmark, composited (centered) onto the format band. +static DWG_LABEL_PNG: &[u8] = include_bytes!("../assets/dwg-label.png"); +/// Format band colour — OCS brand red. +const BAND_RGBA: [u8; 4] = [0xB0, 0x30, 0x20, 0xFF]; + +/// Append a full-width "DWG" band below a thumbnail (rectangular, spanning the +/// whole width) so DWG files read at a glance in the file manager. Grows the +/// image height by the band. Only DWG files ever produce a thumbnail (DXF has no +/// embedded preview and falls back to its file-type icon), so the label is +/// always "DWG". +pub fn badge_dwg(img: &mut RgbaImage) { + let (w, h) = (img.width(), img.height()); + if w == 0 || h == 0 { + return; + } + // Band height ~20% of the thumbnail width (min 18 px). + let band_h = ((w as f32 * 0.20) as u32).max(18); + + // New canvas prefilled with the band colour; the thumbnail covers the top, + // leaving the bottom `band_h` rows as the band. + let mut out = RgbaImage::from_pixel(w, h + band_h, image::Rgba(BAND_RGBA)); + image::imageops::overlay(&mut out, img, 0, 0); + + // Centre the white "DWG" wordmark in the band (~55% of the band height). + if let Ok(label) = image::load_from_memory_with_format(DWG_LABEL_PNG, ImageFormat::Png) { + let label = label.to_rgba8(); + if label.width() > 0 && label.height() > 0 { + let mut target_h = ((band_h as f32 * 0.72) as u32).max(1); + let mut target_w = ((label.width() as f32 * target_h as f32 / label.height() as f32) + as u32) + .max(1); + // Don't let a wide wordmark spill past the thumbnail edges. + let max_w = ((w as f32 * 0.90) as u32).max(1); + if target_w > max_w { + target_w = max_w; + target_h = ((label.height() as f32 * target_w as f32 / label.width() as f32) as u32) + .max(1); + } + let label = image::imageops::thumbnail(&label, target_w, target_h); + let x = ((w as i64 - target_w as i64) / 2).max(0); + let y = h as i64 + (band_h as i64 - target_h as i64) / 2; + image::imageops::overlay(&mut out, &label, x, y); + } + } + + *img = out; +} + +// ── Preview extraction ─────────────────────────────────────────────────────── + +#[derive(Clone, Copy)] +enum Fmt { + Bmp, + Png, +} + +/// DWG preview container start sentinel — the same 16 bytes across all versions. +const PREVIEW_SENTINEL: [u8; 16] = [ + 0x1F, 0x25, 0x6D, 0x07, 0xD4, 0x36, 0x28, 0x28, 0x9D, 0x57, 0xCA, 0x3F, 0x9D, 0x44, 0x10, 0x2B, +]; +/// Image descriptor codes (1 = header, 3 = WMF — both skipped). +const CODE_BMP: u8 = 2; +const CODE_PNG: u8 = 6; + +/// Parse the preview container at the file's raw preview offset and return the +/// first BMP/PNG image. Self-contained byte parsing — the container is a fixed +/// DWG format, so this needs no CAD-library dependency (only `image`). +/// +/// Layout: `[sentinel 16][overall_size RL][count RC] count×[code RC, start RL, +/// size RL] [image data][end sentinel 16]`, where `start` is an ABSOLUTE file +/// offset. +fn read_preview(path: &Path) -> Option<(Fmt, Vec)> { + use std::io::{Read, Seek, SeekFrom}; + let mut f = std::fs::File::open(path).ok()?; + let mut ver = [0u8; 6]; + f.read_exact(&mut ver).ok()?; + if &ver[..2] != b"AC" { + return None; // not a DWG (DXF/other) + } + // Preview seeker: absolute file offset at header byte 0x0D. + f.seek(SeekFrom::Start(0x0D)).ok()?; + let mut a = [0u8; 4]; + f.read_exact(&mut a).ok()?; + let base = i32::from_le_bytes(a); + if base <= 0 { + return None; + } + let base = base as u64; + + // Sentinel + overall size, to learn the container length. + f.seek(SeekFrom::Start(base)).ok()?; + let mut head = [0u8; 20]; + f.read_exact(&mut head).ok()?; + if head[..16] != PREVIEW_SENTINEL { + return None; + } + let overall = u32::from_le_bytes([head[16], head[17], head[18], head[19]]) as usize; + if overall == 0 || overall > 64 * 1024 * 1024 { + return None; + } + // Whole container = sentinel(16) + size(4) + overall + end sentinel(16). + let total = 36 + overall; + f.seek(SeekFrom::Start(base)).ok()?; + let mut buf = vec![0u8; total]; + f.read_exact(&mut buf).ok()?; + + let count = buf[20] as usize; + let mut off = 21usize; + for _ in 0..count { + if off + 9 > buf.len() { + break; + } + let code = buf[off]; + let start = + u32::from_le_bytes([buf[off + 1], buf[off + 2], buf[off + 3], buf[off + 4]]) as u64; + let size = + u32::from_le_bytes([buf[off + 5], buf[off + 6], buf[off + 7], buf[off + 8]]) as usize; + off += 9; + let fmt = match code { + CODE_BMP => Fmt::Bmp, + CODE_PNG => Fmt::Png, + _ => continue, // header / WMF / unknown + }; + // `start` is absolute; translate to a slice offset within `buf`. + let rel = start.checked_sub(base)? as usize; + let end = rel.checked_add(size)?; + if size == 0 || end > buf.len() { + continue; + } + return Some((fmt, buf[rel..end].to_vec())); + } + None +} + +// ── Decode + scale ─────────────────────────────────────────────────────────── + +fn decode(format: Fmt, data: &[u8]) -> Option { + let img = match format { + Fmt::Png => image::load_from_memory_with_format(data, ImageFormat::Png).ok()?, + Fmt::Bmp => image::load_from_memory_with_format(&dib_to_bmp(data), ImageFormat::Bmp).ok()?, + }; + Some(img.to_rgba8()) +} + +fn downscale(img: RgbaImage, max_dim: u32) -> RgbaImage { + let (w, h) = (img.width(), img.height()); + if w <= max_dim && h <= max_dim { + return img; + } + let (nw, nh) = if w >= h { + (max_dim, ((h * max_dim) / w).max(1)) + } else { + (((w * max_dim) / h).max(1), max_dim) + }; + image::imageops::thumbnail(&img, nw, nh) +} + +// ── C ABI (for the macOS QuickLook extension and other FFI consumers) ──────── + +/// Extract a DWG preview and encode it as a PNG. Writes a freshly-allocated +/// buffer to `*out_ptr` / `*out_len`; free it with [`dwg_thumbnail_free`]. +/// Returns `true` on success. `path_utf8` is a NUL-terminated UTF-8 path. +/// +/// # Safety +/// `path_utf8` must be a valid NUL-terminated string; `out_ptr`/`out_len` must +/// be valid, writable pointers. +#[no_mangle] +pub unsafe extern "C" fn dwg_thumbnail_png( + path_utf8: *const std::os::raw::c_char, + max_dim: u32, + out_ptr: *mut *mut u8, + out_len: *mut usize, +) -> bool { + if path_utf8.is_null() || out_ptr.is_null() || out_len.is_null() { + return false; + } + let cstr = std::ffi::CStr::from_ptr(path_utf8); + let Ok(path) = cstr.to_str() else { return false }; + let Some(mut img) = extract(Path::new(path), max_dim) else { + return false; + }; + badge_dwg(&mut img); // full-width "DWG" band, same as every file-manager path + let mut buf = std::io::Cursor::new(Vec::new()); + if img.write_to(&mut buf, ImageFormat::Png).is_err() { + return false; + } + let mut bytes = buf.into_inner().into_boxed_slice(); + *out_ptr = bytes.as_mut_ptr(); + *out_len = bytes.len(); + std::mem::forget(bytes); + true +} + +/// Free a buffer returned by [`dwg_thumbnail_png`]. +/// +/// # Safety +/// `ptr`/`len` must be exactly the values written by a prior successful +/// `dwg_thumbnail_png`, and must be freed at most once. +#[no_mangle] +pub unsafe extern "C" fn dwg_thumbnail_free(ptr: *mut u8, len: usize) { + if !ptr.is_null() && len != 0 { + drop(Box::from_raw(std::slice::from_raw_parts_mut(ptr, len))); + } +} + +/// Prepend the 14-byte `BITMAPFILEHEADER` a stored DIB lacks so a BMP decoder +/// can read it. +fn dib_to_bmp(dib: &[u8]) -> Vec { + if dib.len() < 16 { + return Vec::new(); + } + let bi_size = u32::from_le_bytes([dib[0], dib[1], dib[2], dib[3]]) as usize; + let bpp = u16::from_le_bytes([dib[14], dib[15]]) as usize; + let palette = if (1..=8).contains(&bpp) { (1usize << bpp) * 4 } else { 0 }; + let mut v = Vec::with_capacity(14 + dib.len()); + v.extend_from_slice(b"BM"); + v.extend_from_slice(&((14 + dib.len()) as u32).to_le_bytes()); + v.extend_from_slice(&0u32.to_le_bytes()); + v.extend_from_slice(&((14 + bi_size + palette) as u32).to_le_bytes()); + v.extend_from_slice(dib); + v +} diff --git a/packaging/Info.plist b/packaging/Info.plist index a7f7913f8..ecfb4d334 100644 --- a/packaging/Info.plist +++ b/packaging/Info.plist @@ -35,6 +35,8 @@ AutoCAD Drawing CFBundleTypeRole Editor + CFBundleTypeIconFile + DWG LSItemContentTypes com.autodesk.dwg @@ -47,6 +49,8 @@ AutoCAD DXF CFBundleTypeRole Editor + CFBundleTypeIconFile + DXF LSItemContentTypes com.autodesk.dxf diff --git a/packaging/windows/main.wxs b/packaging/windows/main.wxs index ec91c907f..66ec8de9a 100644 --- a/packaging/windows/main.wxs +++ b/packaging/windows/main.wxs @@ -84,6 +84,20 @@ Advertise='yes' /> + + + + + + +