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:
Hakan Seven 2026-07-11 01:27:17 +03:00
commit ba3b3202e8
20 changed files with 1085 additions and 76 deletions

View 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",
] }

View 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(())
}

View 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"] }

View 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.

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

View 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>

View 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)
}
}

View 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 */

View 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
}