Skip to main content

libvisio_rs/
lib.rs

1#![allow(
2    unused_assignments,
3    clippy::too_many_arguments,
4    clippy::type_complexity,
5    clippy::only_used_in_recursion,
6    clippy::collapsible_else_if,
7    clippy::collapsible_if,
8    clippy::if_same_then_else,
9    clippy::field_reassign_with_default
10)]
11//! libvisio-rs — A Rust library for parsing Microsoft Visio files and converting to SVG.
12//!
13//! Supports both .vsdx (ZIP+XML, Open Packaging) and .vsd (OLE2 binary) formats.
14//! Produces high-quality SVG output with themes, gradients, shadows, rich text,
15//! embedded images, connectors, and more.
16//!
17//! # Examples
18//!
19//! ```no_run
20//! use libvisio_rs::{convert, get_page_info, extract_text};
21//!
22//! // Convert all pages to SVG
23//! let svg_files = convert("diagram.vsdx", Some("/tmp/output"), None).unwrap();
24//!
25//! // Get page info
26//! let pages = get_page_info("diagram.vsdx").unwrap();
27//!
28//! // Extract text
29//! let text = extract_text("diagram.vsdx").unwrap();
30//! ```
31
32pub mod error;
33pub mod model;
34pub mod svg;
35pub mod vsd;
36pub mod vsdx;
37
38use crate::error::{Result, VisioError};
39use crate::model::*;
40use crate::svg::render;
41use std::path::Path;
42
43/// Supported file extensions for XML-based formats.
44pub const XML_EXTENSIONS: &[&str] = &[".vsdx", ".vstx", ".vssx", ".vsdm", ".vstm", ".vssm"];
45
46/// Supported file extensions for binary formats.
47pub const BINARY_EXTENSIONS: &[&str] = &[".vsd", ".vss", ".vst"];
48
49/// All supported file extensions.
50pub const ALL_EXTENSIONS: &[&str] = &[
51    ".vsdx", ".vstx", ".vssx", ".vsdm", ".vstm", ".vssm", ".vsd", ".vss", ".vst",
52];
53
54/// Check if a file extension is supported.
55pub fn is_supported(path: &str) -> bool {
56    let ext = Path::new(path)
57        .extension()
58        .and_then(|e| e.to_str())
59        .unwrap_or("");
60    let dotted = format!(".{}", ext.to_lowercase());
61    ALL_EXTENSIONS.contains(&dotted.as_str())
62}
63
64/// Parse a Visio file and return the parsed document.
65pub fn parse(path: &str) -> Result<Document> {
66    let data = std::fs::read(path)?;
67    let ext = Path::new(path)
68        .extension()
69        .and_then(|e| e.to_str())
70        .map(|e| e.to_lowercase())
71        .unwrap_or_default();
72    let dotted = format!(".{}", ext);
73
74    if XML_EXTENSIONS.contains(&dotted.as_str()) {
75        vsdx::parser::parse_vsdx(&data)
76    } else if BINARY_EXTENSIONS.contains(&dotted.as_str()) {
77        vsd::parser::parse_vsd(&data)
78    } else {
79        Err(VisioError::UnsupportedFormat(format!(
80            "Unsupported format: .{}",
81            ext
82        )))
83    }
84}
85
86/// Convert a Visio file to SVG pages.
87///
88/// Returns a list of SVG file paths (one per page).
89pub fn convert(
90    input_path: &str,
91    output_dir: Option<&str>,
92    page: Option<usize>,
93) -> Result<Vec<String>> {
94    let out_dir = output_dir.unwrap_or("/tmp/libvisio_rs_output");
95    std::fs::create_dir_all(out_dir)?;
96
97    let doc = parse(input_path)?;
98    let basename = Path::new(input_path)
99        .file_stem()
100        .and_then(|s| s.to_str())
101        .unwrap_or("visio");
102
103    let mut svg_files = Vec::new();
104
105    for p in &doc.pages {
106        if let Some(page_num) = page {
107            if p.index != page_num {
108                continue;
109            }
110        }
111
112        // Get background shapes if any
113        let bg_shapes: Option<Vec<Shape>> = doc
114            .background_map
115            .get(&p.index)
116            .and_then(|bg_idx| doc.pages.iter().find(|pp| pp.index == *bg_idx))
117            .map(|bg_page| bg_page.shapes.clone());
118
119        let svg_content = render::shapes_to_svg(
120            &p.shapes,
121            p.width,
122            p.height,
123            &doc.masters,
124            &p.connects,
125            &doc.media,
126            &std::collections::HashMap::new(),
127            bg_shapes.as_deref(),
128            &doc.theme_colors,
129            &p.layers,
130        );
131
132        let svg_path = format!("{}/{}_page{}.svg", out_dir, basename, p.index + 1);
133        std::fs::write(&svg_path, &svg_content)?;
134        svg_files.push(svg_path);
135    }
136
137    Ok(svg_files)
138}
139
140/// Convert a single page to SVG string (no file output).
141pub fn convert_page_to_svg(input_path: &str, page_index: usize) -> Result<String> {
142    let doc = parse(input_path)?;
143    let page = doc
144        .pages
145        .iter()
146        .find(|p| p.index == page_index)
147        .ok_or(VisioError::PageNotFound(page_index))?;
148
149    let bg_shapes: Option<Vec<Shape>> = doc
150        .background_map
151        .get(&page.index)
152        .and_then(|bg_idx| doc.pages.iter().find(|pp| pp.index == *bg_idx))
153        .map(|bg_page| bg_page.shapes.clone());
154
155    Ok(render::shapes_to_svg(
156        &page.shapes,
157        page.width,
158        page.height,
159        &doc.masters,
160        &page.connects,
161        &doc.media,
162        &std::collections::HashMap::new(),
163        bg_shapes.as_deref(),
164        &doc.theme_colors,
165        &page.layers,
166    ))
167}
168
169/// Get page information from a Visio file.
170pub fn get_page_info(path: &str) -> Result<Vec<PageInfo>> {
171    let doc = parse(path)?;
172    Ok(doc
173        .pages
174        .iter()
175        .map(|p| PageInfo {
176            name: p.name.clone(),
177            index: p.index,
178            width: p.width,
179            height: p.height,
180        })
181        .collect())
182}
183
184/// Extract all text from a Visio file.
185pub fn extract_text(path: &str) -> Result<String> {
186    let doc = parse(path)?;
187    let mut text_lines = Vec::new();
188    for page in &doc.pages {
189        text_lines.push(format!(
190            "--- {} ---",
191            if page.name.is_empty() {
192                format!("Page {}", page.index + 1)
193            } else {
194                page.name.clone()
195            }
196        ));
197        for shape in &page.shapes {
198            extract_shape_text(&mut text_lines, shape);
199        }
200        text_lines.push(String::new());
201    }
202    Ok(text_lines.join("\n"))
203}
204
205fn extract_shape_text(lines: &mut Vec<String>, shape: &Shape) {
206    if !shape.text.is_empty() {
207        lines.push(shape.text.clone());
208    }
209    for sub in &shape.sub_shapes {
210        extract_shape_text(lines, sub);
211    }
212}
213
214// ============================================================
215// C ABI — extern "C" functions for FFI
216// ============================================================
217
218use std::ffi::{CStr, CString};
219use std::os::raw::c_char;
220
221/// Opaque handle to a parsed Visio document.
222#[repr(C)]
223pub struct VisioDocument {
224    inner: Document,
225}
226
227/// Open and parse a Visio file. Returns null on error.
228#[no_mangle]
229/// # Safety
230/// `path` must be a valid null-terminated C string.
231pub unsafe extern "C" fn visio_open(path: *const c_char) -> *mut VisioDocument {
232    if path.is_null() {
233        return std::ptr::null_mut();
234    }
235    let path_str = match unsafe { CStr::from_ptr(path) }.to_str() {
236        Ok(s) => s,
237        Err(_) => return std::ptr::null_mut(),
238    };
239    match parse(path_str) {
240        Ok(doc) => Box::into_raw(Box::new(VisioDocument { inner: doc })),
241        Err(_) => std::ptr::null_mut(),
242    }
243}
244
245/// Get the number of pages.
246#[no_mangle]
247/// # Safety
248/// `doc` must be a valid pointer returned by `visio_open`.
249pub unsafe extern "C" fn visio_get_page_count(doc: *const VisioDocument) -> usize {
250    if doc.is_null() {
251        return 0;
252    }
253    unsafe { &*doc }.inner.pages.len()
254}
255
256/// Convert a page to SVG. Returns null on error.
257/// Caller must free with visio_free_string.
258#[no_mangle]
259/// # Safety
260/// `doc` must be a valid pointer returned by `visio_open`.
261pub unsafe extern "C" fn visio_convert_page_to_svg(
262    doc: *const VisioDocument,
263    page: usize,
264) -> *mut c_char {
265    if doc.is_null() {
266        return std::ptr::null_mut();
267    }
268    let document = &unsafe { &*doc }.inner;
269    let p = match document.pages.iter().find(|p| p.index == page) {
270        Some(p) => p,
271        None => return std::ptr::null_mut(),
272    };
273
274    let bg_shapes: Option<Vec<Shape>> = document
275        .background_map
276        .get(&p.index)
277        .and_then(|bg_idx| document.pages.iter().find(|pp| pp.index == *bg_idx))
278        .map(|bg_page| bg_page.shapes.clone());
279
280    let svg = render::shapes_to_svg(
281        &p.shapes,
282        p.width,
283        p.height,
284        &document.masters,
285        &p.connects,
286        &document.media,
287        &std::collections::HashMap::new(),
288        bg_shapes.as_deref(),
289        &document.theme_colors,
290        &p.layers,
291    );
292
293    match CString::new(svg) {
294        Ok(c) => c.into_raw(),
295        Err(_) => std::ptr::null_mut(),
296    }
297}
298
299/// Extract all text from the document.
300/// Caller must free with visio_free_string.
301#[no_mangle]
302/// # Safety
303/// `doc` must be a valid pointer returned by `visio_open`.
304pub unsafe extern "C" fn visio_extract_text(doc: *const VisioDocument) -> *mut c_char {
305    if doc.is_null() {
306        return std::ptr::null_mut();
307    }
308    let document = &unsafe { &*doc }.inner;
309    let mut lines = Vec::new();
310    for page in &document.pages {
311        lines.push(format!(
312            "--- {} ---",
313            if page.name.is_empty() {
314                format!("Page {}", page.index + 1)
315            } else {
316                page.name.clone()
317            }
318        ));
319        for shape in &page.shapes {
320            if !shape.text.is_empty() {
321                lines.push(shape.text.clone());
322            }
323        }
324    }
325    match CString::new(lines.join("\n")) {
326        Ok(c) => c.into_raw(),
327        Err(_) => std::ptr::null_mut(),
328    }
329}
330
331/// Free a VisioDocument handle.
332/// # Safety
333/// `doc` must be a valid pointer from `visio_open`, or null.
334#[no_mangle]
335pub unsafe extern "C" fn visio_free(doc: *mut VisioDocument) {
336    if !doc.is_null() {
337        unsafe {
338            let _ = Box::from_raw(doc);
339        }
340    }
341}
342
343/// Free a string returned by visio_convert_page_to_svg or visio_extract_text.
344#[no_mangle]
345/// # Safety
346/// `s` must be a valid pointer returned by a `visio_*` function, or null.
347pub unsafe extern "C" fn visio_free_string(s: *mut c_char) {
348    if !s.is_null() {
349        unsafe {
350            let _ = CString::from_raw(s);
351        }
352    }
353}