diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 86ce62b4..50c88047 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 7f80eda2..9fc3736c 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 57e16802..a0071cfb 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 00000000..64f4f704
--- /dev/null
+++ b/assets/mimetypes/image-vnd.dwg.svg
@@ -0,0 +1,15 @@
+
+
+
diff --git a/assets/mimetypes/image-vnd.dxf.svg b/assets/mimetypes/image-vnd.dxf.svg
new file mode 100644
index 00000000..988f8831
--- /dev/null
+++ b/assets/mimetypes/image-vnd.dxf.svg
@@ -0,0 +1,15 @@
+
+
+
diff --git a/crates/dwg-thumbnailer-win/Cargo.toml b/crates/dwg-thumbnailer-win/Cargo.toml
new file mode 100644
index 00000000..9750103e
--- /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 00000000..07f8310d
--- /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