feat(thumbnails): embed DWG previews and wire cross-platform file thumbnails
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 <noreply@anthropic.com>
This commit is contained in:
parent
5c4ba97c5f
commit
ba3b3202e8
20 changed files with 1085 additions and 76 deletions
59
.github/workflows/release.yml
vendored
59
.github/workflows/release.yml
vendored
|
|
@ -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}"
|
||||
|
|
|
|||
16
Cargo.lock
generated
16
Cargo.lock
generated
|
|
@ -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"
|
||||
|
|
|
|||
12
Cargo.toml
12
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"
|
||||
|
|
|
|||
15
assets/mimetypes/image-vnd.dwg.svg
Normal file
15
assets/mimetypes/image-vnd.dwg.svg
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- OpenCADStudio DWG file-type icon (freedesktop mimetypes/image-vnd.dwg). -->
|
||||
<svg viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg">
|
||||
<!-- Page -->
|
||||
<path d="M12 3 H30 L40 13 V43 Q40 45 38 45 H12 Q10 45 10 43 V5 Q10 3 12 3 Z"
|
||||
fill="#F4F4F5" stroke="#C4C4C8" stroke-width="0.8"/>
|
||||
<!-- Folded corner -->
|
||||
<path d="M30 3 V13 H40 Z" fill="#D9D9DC"/>
|
||||
<path d="M30 3 V13 H40" fill="none" stroke="#C4C4C8" stroke-width="0.8"/>
|
||||
<!-- Format band -->
|
||||
<rect x="7" y="27" width="34" height="13" rx="2" fill="#B03020"/>
|
||||
<text x="24" y="36.5" font-family="'DejaVu Sans','Helvetica',sans-serif"
|
||||
font-weight="700" font-size="9" letter-spacing="0.5"
|
||||
fill="#FFFFFF" text-anchor="middle">DWG</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 788 B |
15
assets/mimetypes/image-vnd.dxf.svg
Normal file
15
assets/mimetypes/image-vnd.dxf.svg
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- OpenCADStudio DXF file-type icon (freedesktop mimetypes/image-vnd.dxf). -->
|
||||
<svg viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg">
|
||||
<!-- Page -->
|
||||
<path d="M12 3 H30 L40 13 V43 Q40 45 38 45 H12 Q10 45 10 43 V5 Q10 3 12 3 Z"
|
||||
fill="#F4F4F5" stroke="#C4C4C8" stroke-width="0.8"/>
|
||||
<!-- Folded corner -->
|
||||
<path d="M30 3 V13 H40 Z" fill="#D9D9DC"/>
|
||||
<path d="M30 3 V13 H40" fill="none" stroke="#C4C4C8" stroke-width="0.8"/>
|
||||
<!-- Format band -->
|
||||
<rect x="7" y="27" width="34" height="13" rx="2" fill="#2C6FB0"/>
|
||||
<text x="24" y="36.5" font-family="'DejaVu Sans','Helvetica',sans-serif"
|
||||
font-weight="700" font-size="9" letter-spacing="0.5"
|
||||
fill="#FFFFFF" text-anchor="middle">DXF</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 788 B |
26
crates/dwg-thumbnailer-win/Cargo.toml
Normal file
26
crates/dwg-thumbnailer-win/Cargo.toml
Normal file
|
|
@ -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",
|
||||
] }
|
||||
279
crates/dwg-thumbnailer-win/src/lib.rs
Normal file
279
crates/dwg-thumbnailer-win/src/lib.rs
Normal file
|
|
@ -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<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: &image::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::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<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(())
|
||||
}
|
||||
19
crates/dwg-thumbnailer/Cargo.toml
Normal file
19
crates/dwg-thumbnailer/Cargo.toml
Normal file
|
|
@ -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"] }
|
||||
80
crates/dwg-thumbnailer/README.md
Normal file
80
crates/dwg-thumbnailer/README.md
Normal file
|
|
@ -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.
|
||||
BIN
crates/dwg-thumbnailer/assets/dwg-label.png
Normal file
BIN
crates/dwg-thumbnailer/assets/dwg-label.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.3 KiB |
42
crates/dwg-thumbnailer/macos/Info.plist
Normal file
42
crates/dwg-thumbnailer/macos/Info.plist
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<!-- Info.plist for the DWG QuickLook Thumbnail Extension (.appex). -->
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>DWG Thumbnails</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>DWGThumbnail</string>
|
||||
<!-- Must be prefixed with the host app id so it registers as its extension. -->
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>io.github.HakanSeven12.OpenCadStudio.DWGThumbnail</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>DWGThumbnail</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>XPC!</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>__VERSION__</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>__VERSION__</string>
|
||||
<key>NSExtension</key>
|
||||
<dict>
|
||||
<key>NSExtensionPointIdentifier</key>
|
||||
<string>com.apple.quicklook.thumbnail</string>
|
||||
<key>NSExtensionPrincipalClass</key>
|
||||
<string>DWGThumbnail.ThumbnailProvider</string>
|
||||
<key>NSExtensionAttributes</key>
|
||||
<dict>
|
||||
<key>QLSupportedContentTypes</key>
|
||||
<array>
|
||||
<string>com.autodesk.dwg</string>
|
||||
</array>
|
||||
<key>QLThumbnailMinimumDimension</key>
|
||||
<integer>0</integer>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
49
crates/dwg-thumbnailer/macos/ThumbnailProvider.swift
Normal file
49
crates/dwg-thumbnailer/macos/ThumbnailProvider.swift
Normal file
|
|
@ -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<UInt8>? = 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)
|
||||
}
|
||||
}
|
||||
18
crates/dwg-thumbnailer/macos/dwg_thumbnailer.h
Normal file
18
crates/dwg-thumbnailer/macos/dwg_thumbnailer.h
Normal file
|
|
@ -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 <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
/* 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 */
|
||||
247
crates/dwg-thumbnailer/src/lib.rs
Normal file
247
crates/dwg-thumbnailer/src/lib.rs
Normal file
|
|
@ -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<RgbaImage> {
|
||||
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<u8>)> {
|
||||
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<RgbaImage> {
|
||||
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<u8> {
|
||||
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
|
||||
}
|
||||
|
|
@ -35,6 +35,8 @@
|
|||
<string>AutoCAD Drawing</string>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Editor</string>
|
||||
<key>CFBundleTypeIconFile</key>
|
||||
<string>DWG</string>
|
||||
<key>LSItemContentTypes</key>
|
||||
<array>
|
||||
<string>com.autodesk.dwg</string>
|
||||
|
|
@ -47,6 +49,8 @@
|
|||
<string>AutoCAD DXF</string>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Editor</string>
|
||||
<key>CFBundleTypeIconFile</key>
|
||||
<string>DXF</string>
|
||||
<key>LSItemContentTypes</key>
|
||||
<array>
|
||||
<string>com.autodesk.dxf</string>
|
||||
|
|
|
|||
|
|
@ -84,6 +84,20 @@
|
|||
Advertise='yes' />
|
||||
</File>
|
||||
|
||||
<!-- DWG/DXF file-type icons, generated from the mimetype SVGs
|
||||
(CI: magick image-vnd.{dwg,dxf}.svg -> {dwg,dxf}.ico). -->
|
||||
<File Id='DwgIco' Name='dwg.ico' DiskId='1' Source='packaging\windows\dwg.ico' />
|
||||
<File Id='DxfIco' Name='dxf.ico' DiskId='1' Source='packaging\windows\dxf.ico' />
|
||||
|
||||
<!-- DWG thumbnail provider (IThumbnailProvider). Ships beside the
|
||||
exe; OpenCADStudio self-registers it under HKCU on startup
|
||||
(see file_association::install_thumbnailer). -->
|
||||
<File
|
||||
Id='DwgThumbDll'
|
||||
Name='dwg_thumbnailer_win.dll'
|
||||
DiskId='1'
|
||||
Source='target\release\dwg_thumbnailer_win.dll' />
|
||||
|
||||
<!--
|
||||
File associations. Two ProgIDs (one per extension) so the
|
||||
`<Verb Id='open'>` rows don't collide on the same registry
|
||||
|
|
@ -93,7 +107,7 @@
|
|||
<ProgId
|
||||
Id='OpenCADStudio.DWG'
|
||||
Description='DWG Drawing'
|
||||
Icon='OpenCADStudioEXE'
|
||||
Icon='DwgIco'
|
||||
IconIndex='0'>
|
||||
<Extension Id='dwg' ContentType='image/vnd.dwg'>
|
||||
<Verb Id='open' Command='Open' TargetFile='OpenCADStudioEXE' Argument='"%1"' />
|
||||
|
|
@ -103,7 +117,7 @@
|
|||
<ProgId
|
||||
Id='OpenCADStudio.DXF'
|
||||
Description='DXF Drawing'
|
||||
Icon='OpenCADStudioEXE'
|
||||
Icon='DxfIco'
|
||||
IconIndex='0'>
|
||||
<Extension Id='dxf' ContentType='image/vnd.dxf'>
|
||||
<Verb Id='open' Command='Open' TargetFile='OpenCADStudioEXE' Argument='"%1"' />
|
||||
|
|
|
|||
|
|
@ -67,6 +67,11 @@ pub struct Cli {
|
|||
/// Internal: run as the plugin runner child process.
|
||||
#[arg(long, value_names = ["SOCKET", "CDYLIB"], num_args = 2, hide = true)]
|
||||
pub ocs_plugin_runner: Option<Vec<String>>,
|
||||
|
||||
/// Internal: write a DWG's embedded preview to a PNG for the OS file-manager
|
||||
/// thumbnailer (`<IN> <OUT> <SIZE>`). Handled before the GUI starts.
|
||||
#[arg(long, value_names = ["IN", "OUT", "SIZE"], num_args = 3, hide = true)]
|
||||
pub dwg_thumbnail: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
/// GUI startup configuration, handed from `main` to `app::boot` out-of-band
|
||||
|
|
|
|||
|
|
@ -80,6 +80,26 @@ pub fn unregister_handler() -> Result<(), String> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Register (or refresh) the OS file-manager integration for DWG/DXF, so files
|
||||
/// show OCS thumbnails (embedded preview) or, when there is none, a distinct
|
||||
/// OCS DWG/DXF icon. Idempotent, best-effort, silent. On Linux this writes a
|
||||
/// freedesktop `.thumbnailer` (pointing at this binary's `--dwg-thumbnail` mode)
|
||||
/// plus the mimetype icons under `$XDG_DATA_HOME`; on Windows it registers the
|
||||
/// bundled thumbnail-provider DLL.
|
||||
pub fn install_thumbnailer() {
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
let _ = linux_impl::install_thumbnailer();
|
||||
let _ = linux_impl::install_mime_icons();
|
||||
}
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
let _ = windows_impl::install_thumbnailer();
|
||||
}
|
||||
// macOS: the QuickLook thumbnail extension is bundled inside the .app and
|
||||
// registered by LaunchServices automatically — nothing to do at runtime.
|
||||
}
|
||||
|
||||
/// Try to make this app the default handler for .dwg and .dxf. Returns a short
|
||||
/// human-readable status string on success, or an error message on failure.
|
||||
pub async fn set_default_app() -> Result<String, String> {
|
||||
|
|
@ -233,6 +253,16 @@ mod windows_impl {
|
|||
RegDeleteTreeW(HKEY_CURRENT_USER, w.as_ptr());
|
||||
}
|
||||
}
|
||||
// Also drop the DWG thumbnail-provider registration.
|
||||
for key in [
|
||||
format!(r"Software\Classes\CLSID\{THUMB_CLSID}"),
|
||||
format!(r"Software\Classes\.dwg\ShellEx\{THUMB_IID}"),
|
||||
] {
|
||||
let w = wide(&key);
|
||||
unsafe {
|
||||
RegDeleteTreeW(HKEY_CURRENT_USER, w.as_ptr());
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
@ -276,6 +306,41 @@ mod windows_impl {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// CLSID of the DWG thumbnail provider (must match `dwg-thumbnailer-win`).
|
||||
const THUMB_CLSID: &str = "{8F2A9C41-3B6E-4E2D-9C7A-1E0B5D6F42AA}";
|
||||
/// The shell interface id Explorer looks up under `.dwg\ShellEx`.
|
||||
const THUMB_IID: &str = "{e357fccd-a995-4576-b01f-234630154e96}";
|
||||
|
||||
/// Register the bundled DWG thumbnail provider DLL under `HKCU` — the same
|
||||
/// keys `regsvr32` would write, but per-user (no admin) and driven by this
|
||||
/// app on startup. No-op if `dwg_thumbnailer_win.dll` isn't shipped next to
|
||||
/// the exe. Idempotent (the values are stable across launches).
|
||||
pub(super) fn install_thumbnailer() -> Result<(), String> {
|
||||
let exe = std::env::current_exe().map_err(|e| e.to_string())?;
|
||||
let dll = exe
|
||||
.parent()
|
||||
.ok_or("no executable directory")?
|
||||
.join("dwg_thumbnailer_win.dll");
|
||||
if !dll.exists() {
|
||||
return Ok(()); // provider DLL not bundled — nothing to register
|
||||
}
|
||||
let dll = dll.to_string_lossy();
|
||||
let clsid_base = format!(r"Software\Classes\CLSID\{THUMB_CLSID}");
|
||||
set_string(&clsid_base, None, "OpenCADStudio DWG Thumbnail Provider")?;
|
||||
set_string(&format!(r"{clsid_base}\InprocServer32"), None, &dll)?;
|
||||
set_string(
|
||||
&format!(r"{clsid_base}\InprocServer32"),
|
||||
Some("ThreadingModel"),
|
||||
"Apartment",
|
||||
)?;
|
||||
set_string(
|
||||
&format!(r"Software\Classes\.dwg\ShellEx\{THUMB_IID}"),
|
||||
None,
|
||||
THUMB_CLSID,
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Create a per-user ProgID (`HKCU\Software\Classes\<progid>`) with an icon
|
||||
/// and an open command, mirroring one of the MSI's per-machine ProgIDs.
|
||||
fn register_progid(exe: &str, progid: &str, description: &str) -> Result<(), String> {
|
||||
|
|
@ -372,6 +437,33 @@ mod linux_impl {
|
|||
use super::APP_ID;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
/// Write (idempotently) a `.thumbnailer` that points this binary's
|
||||
/// `--dwg-thumbnail` mode at DWG MIME types, so file managers render the
|
||||
/// embedded preview. `~/.local/bin` need not be on PATH — the entry uses the
|
||||
/// binary's absolute path (or `$APPIMAGE`).
|
||||
pub(super) fn install_thumbnailer() -> Result<(), String> {
|
||||
let exec = std::env::var_os("APPIMAGE")
|
||||
.map(PathBuf::from)
|
||||
.or_else(|| std::env::current_exe().ok())
|
||||
.ok_or("could not determine the executable path")?;
|
||||
let exec = exec.to_string_lossy();
|
||||
let dir = data_home()?.join("thumbnailers");
|
||||
let path = dir.join(format!("{APP_ID}-dwg.thumbnailer"));
|
||||
let contents = format!(
|
||||
"[Thumbnailer Entry]\n\
|
||||
TryExec={exec}\n\
|
||||
Exec={exec} --dwg-thumbnail %i %o %s\n\
|
||||
MimeType=image/vnd.dwg;image/x-dwg;application/x-dwg;application/acad;application/x-autocad;\n"
|
||||
);
|
||||
// Unchanged since last launch → skip the write.
|
||||
if std::fs::read_to_string(&path).ok().as_deref() == Some(contents.as_str()) {
|
||||
return Ok(());
|
||||
}
|
||||
std::fs::create_dir_all(&dir).map_err(|e| e.to_string())?;
|
||||
std::fs::write(&path, &contents).map_err(|e| e.to_string())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Write a user-level .desktop file pointing at the running binary, then
|
||||
/// refresh the MIME cache so file managers list us under "Open with".
|
||||
/// Skipped when a system package already registered the app, so we don't
|
||||
|
|
@ -542,6 +634,38 @@ mod linux_impl {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Install the OCS DWG/DXF file-type icons into the user's hicolor theme
|
||||
/// (`mimetypes/image-vnd.dwg.svg`, `...dxf.svg`), so file managers show a
|
||||
/// distinct icon for DWG/DXF files that have no thumbnail. Idempotent.
|
||||
pub(super) fn install_mime_icons() -> Result<(), String> {
|
||||
static DWG_ICON: &[u8] = include_bytes!("../../assets/mimetypes/image-vnd.dwg.svg");
|
||||
static DXF_ICON: &[u8] = include_bytes!("../../assets/mimetypes/image-vnd.dxf.svg");
|
||||
|
||||
let dir = data_home()?.join("icons/hicolor/scalable/mimetypes");
|
||||
let mut changed = false;
|
||||
for (name, bytes) in [
|
||||
("image-vnd.dwg.svg", DWG_ICON),
|
||||
("image-vnd.dxf.svg", DXF_ICON),
|
||||
] {
|
||||
let path = dir.join(name);
|
||||
if std::fs::read(&path).ok().as_deref() == Some(bytes) {
|
||||
continue;
|
||||
}
|
||||
std::fs::create_dir_all(&dir).map_err(|e| e.to_string())?;
|
||||
std::fs::write(&path, bytes).map_err(|e| e.to_string())?;
|
||||
changed = true;
|
||||
}
|
||||
if changed {
|
||||
let hicolor = data_home()
|
||||
.map(|d| d.join("icons/hicolor"))
|
||||
.unwrap_or_default();
|
||||
let _ = std::process::Command::new("gtk-update-icon-cache")
|
||||
.args(["--force", "--quiet", &hicolor.to_string_lossy()])
|
||||
.status();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn data_home() -> Result<PathBuf, String> {
|
||||
std::env::var_os("XDG_DATA_HOME")
|
||||
.map(PathBuf::from)
|
||||
|
|
|
|||
|
|
@ -1,9 +1,12 @@
|
|||
//! CPU wire-raster thumbnail for the DWG preview image.
|
||||
//! DWG preview thumbnails.
|
||||
//!
|
||||
//! Rasterizes the current layout's wire set into a small bitmap and returns it
|
||||
//! as an [`acadrust::Preview`] (a Windows DIB, universally accepted as a DWG
|
||||
//! preview across versions). Embedded on save so OCS drawings show a thumbnail
|
||||
//! in file browsers and other CAD applications.
|
||||
//! - [`from_scene`] / [`from_wires`] rasterize the current layout's wires into a
|
||||
//! small DIB [`acadrust::Preview`], embedded on save so OCS drawings show a
|
||||
//! thumbnail in file browsers and other CAD apps.
|
||||
//! - [`read_handle`] / [`extract_to_png`] read a DWG's *embedded* preview back
|
||||
//! for the Start page and the OS file-manager thumbnailer. Extraction lives in
|
||||
//! the shared [`dwg_thumbnailer`] core crate (also used by the Windows/macOS
|
||||
//! thumbnail handlers).
|
||||
|
||||
use acadrust::{Preview, PreviewFormat};
|
||||
use image::{ImageFormat, Rgb, RgbImage};
|
||||
|
|
@ -114,76 +117,31 @@ fn to_rgb(c: [f32; 4]) -> [u8; 3] {
|
|||
]
|
||||
}
|
||||
|
||||
/// Cheaply read ONLY the embedded preview from a DWG (file header + preview
|
||||
/// bytes, no full document parse) and decode it to an iced image handle.
|
||||
/// `None` for DXF/other files, a missing or empty preview, or a format the
|
||||
/// `image` crate can't decode (WMF). Used to show recent-file thumbnails.
|
||||
pub fn read_handle(path: &std::path::Path) -> Option<iced::widget::image::Handle> {
|
||||
decode(&read_preview(path)?)
|
||||
}
|
||||
|
||||
/// Read the preview image bytes straight from the file's raw preview offset.
|
||||
fn read_preview(path: &std::path::Path) -> Option<Preview> {
|
||||
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 has no thumbnail)
|
||||
}
|
||||
// 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;
|
||||
f.seek(SeekFrom::Start(base)).ok()?;
|
||||
let mut head = [0u8; 20];
|
||||
f.read_exact(&mut head).ok()?;
|
||||
let overall = acadrust::io::dwg::preview::overall_size(&head)?;
|
||||
if overall == 0 || overall > 64 * 1024 * 1024 {
|
||||
return None;
|
||||
}
|
||||
let total = acadrust::io::dwg::preview::container_len(overall);
|
||||
f.seek(SeekFrom::Start(base)).ok()?;
|
||||
let mut buf = vec![0u8; total];
|
||||
f.read_exact(&mut buf).ok()?;
|
||||
acadrust::io::dwg::preview::parse_preview(&buf, base)
|
||||
}
|
||||
|
||||
/// Decode a stored preview to an iced RGBA handle.
|
||||
fn decode(p: &Preview) -> Option<iced::widget::image::Handle> {
|
||||
let img = match p.format {
|
||||
PreviewFormat::Png => image::load_from_memory_with_format(&p.data, ImageFormat::Png).ok()?,
|
||||
PreviewFormat::Bmp => {
|
||||
image::load_from_memory_with_format(&dib_to_bmp(&p.data), ImageFormat::Bmp).ok()?
|
||||
/// Read a DWG's embedded preview and write it as a PNG at `output`, scaled so
|
||||
/// its longest edge is at most `size`. Returns `false` on any failure (no
|
||||
/// preview, undecodable, write error) so the OS thumbnailer falls back to a
|
||||
/// generic icon. Backs the hidden `--dwg-thumbnail` mode the installed
|
||||
/// freedesktop `.thumbnailer` invokes. Extraction lives in the shared
|
||||
/// [`dwg_thumbnailer`] core (also used by the Windows/macOS handlers).
|
||||
pub fn extract_to_png(input: &std::path::Path, output: &std::path::Path, size: u32) -> bool {
|
||||
match dwg_thumbnailer::extract(input, size) {
|
||||
Some(mut img) => {
|
||||
// Bottom-left "DWG" ribbon so the format reads at a glance in the
|
||||
// file manager (the Start-page `read_handle` stays unbadged).
|
||||
dwg_thumbnailer::badge_dwg(&mut img);
|
||||
img.save_with_format(output, ImageFormat::Png).is_ok()
|
||||
}
|
||||
PreviewFormat::Wmf => return None,
|
||||
};
|
||||
let rgba = img.to_rgba8();
|
||||
let (w, h) = (rgba.width(), rgba.height());
|
||||
Some(iced::widget::image::Handle::from_rgba(w, h, rgba.into_raw()))
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Reconstruct a full in-memory BMP from a stored DIB (prepend the 14-byte
|
||||
/// `BITMAPFILEHEADER`) so a BMP decoder can read it.
|
||||
fn dib_to_bmp(dib: &[u8]) -> Vec<u8> {
|
||||
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
|
||||
/// Read a DWG's embedded preview and decode it to an iced image handle for the
|
||||
/// Start page's recent-file thumbnails. `None` for DXF/other files, a missing
|
||||
/// preview, or an undecodable format (WMF).
|
||||
pub fn read_handle(path: &std::path::Path) -> Option<iced::widget::image::Handle> {
|
||||
let img = dwg_thumbnailer::extract(path, MAX_DIM)?;
|
||||
let (w, h) = (img.width(), img.height());
|
||||
Some(iced::widget::image::Handle::from_rgba(w, h, img.into_raw()))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
@ -198,6 +156,17 @@ mod tests {
|
|||
}
|
||||
}
|
||||
|
||||
/// Prepend a `BITMAPFILEHEADER` to a 24-bit DIB so `image` can decode it.
|
||||
fn dib_to_bmp(dib: &[u8]) -> Vec<u8> {
|
||||
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(&54u32.to_le_bytes()); // 14 + 40, no palette (24-bit)
|
||||
v.extend_from_slice(dib);
|
||||
v
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_input_yields_none() {
|
||||
assert!(from_wires(&[], [0.0, 0.0, 0.0, 1.0]).is_none());
|
||||
|
|
|
|||
20
src/main.rs
20
src/main.rs
|
|
@ -51,6 +51,19 @@ fn main() -> iced::Result {
|
|||
return Ok(());
|
||||
}
|
||||
|
||||
// Thumbnail mode: the OS file-manager thumbnailer invokes us as
|
||||
// `--dwg-thumbnail <in> <out> <size>`. Extract the DWG's embedded
|
||||
// preview to a PNG and exit — never touch the GUI.
|
||||
if let Some(a) = &args.dwg_thumbnail {
|
||||
let size = a.get(2).and_then(|s| s.parse().ok()).unwrap_or(256);
|
||||
let ok = io::thumbnail::extract_to_png(
|
||||
std::path::Path::new(&a[0]),
|
||||
std::path::Path::new(&a[1]),
|
||||
size,
|
||||
);
|
||||
std::process::exit(if ok { 0 } else { 1 });
|
||||
}
|
||||
|
||||
// Opt-in logging. `--log LEVEL` seeds RUST_LOG; the subscriber then
|
||||
// surfaces wgpu / iced / winit diagnostics that are otherwise silent.
|
||||
if let Some(level) = &args.log {
|
||||
|
|
@ -110,6 +123,13 @@ fn main() -> iced::Result {
|
|||
read_only: args.read_only,
|
||||
script_lines,
|
||||
});
|
||||
|
||||
// Register (or refresh) the freedesktop DWG thumbnailer so file managers
|
||||
// show OCS-embedded previews. Idempotent, best-effort, no consent step —
|
||||
// it only points a `.thumbnailer` at this same binary's `--dwg-thumbnail`
|
||||
// mode. Silently ignored on failure or non-Linux.
|
||||
io::file_association::install_thumbnailer();
|
||||
|
||||
app::run()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue