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)]
11pub 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
43pub const XML_EXTENSIONS: &[&str] = &[".vsdx", ".vstx", ".vssx", ".vsdm", ".vstm", ".vssm"];
45
46pub const BINARY_EXTENSIONS: &[&str] = &[".vsd", ".vss", ".vst"];
48
49pub const ALL_EXTENSIONS: &[&str] = &[
51 ".vsdx", ".vstx", ".vssx", ".vsdm", ".vstm", ".vssm", ".vsd", ".vss", ".vst",
52];
53
54pub 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
64pub 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
86pub 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 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
140pub 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
169pub 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
184pub 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
214use std::ffi::{CStr, CString};
219use std::os::raw::c_char;
220
221#[repr(C)]
223pub struct VisioDocument {
224 inner: Document,
225}
226
227#[no_mangle]
229pub 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#[no_mangle]
247pub 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#[no_mangle]
259pub 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#[no_mangle]
302pub 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#[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#[no_mangle]
345pub 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}