Skip to main content

document_svg/document/
vdx.rs

1//! Legacy Visio XML Drawing (`.vdx`) page preview.
2//!
3//! VDX carries the same ShapeSheet vocabulary as VSDX but stores pages in one
4//! XML document instead of an OPC ZIP package. Each Page subtree is streamed
5//! into the shared bounded Visio page renderer; no macros or external links
6//! are evaluated.
7
8use std::fs::File;
9use std::io::Read;
10use std::path::Path;
11
12use quick_xml::Reader;
13use quick_xml::Writer;
14use quick_xml::events::{BytesEnd, BytesStart, Event};
15
16use crate::convert::{ConvertOptions, PageConsumer};
17use crate::document::vsdx;
18use crate::error::{Error, Result};
19use crate::ooxml::{attribute, local_name};
20
21const MAX_VDX_PAGE_BYTES: usize = 32 * 1024 * 1024;
22const MAX_VDX_PAGES: usize = 10_000;
23const MAX_VDX_DEPTH: usize = 256;
24
25#[derive(Debug)]
26struct VdxPage {
27    id: String,
28    name: String,
29    is_background: bool,
30    references_background: bool,
31}
32
33pub(crate) fn convert(
34    path: &Path,
35    options: &ConvertOptions,
36    sink: &mut dyn PageConsumer,
37) -> Result<Vec<String>> {
38    let max_bytes = options.max_input_bytes.min(512 * 1024 * 1024);
39    let mut bytes = Vec::new();
40    Read::take(File::open(path)?, max_bytes.saturating_add(1)).read_to_end(&mut bytes)?;
41    if bytes.len() as u64 > max_bytes {
42        return Err(Error::LimitExceeded(format!(
43            "VDX input exceeds maximum limit of {max_bytes} bytes"
44        )));
45    }
46
47    let mut reader = Reader::from_reader(bytes.as_slice());
48    reader.config_mut().trim_text(false);
49    let mut buffer = Vec::new();
50    let mut root_namespaces = Vec::<(String, String)>::new();
51    let mut root_seen = false;
52    let mut depth = 0usize;
53    let mut events = 0usize;
54    let mut page_entry_count = 0usize;
55    let mut rendered_page_count = 0usize;
56    let mut writer: Option<Writer<Vec<u8>>> = None;
57    let mut page: Option<VdxPage> = None;
58    let mut page_depth = 0usize;
59    let mut warnings = Vec::new();
60    let mut saw_background = false;
61
62    loop {
63        events += 1;
64        if events > options.max_xml_events {
65            return Err(Error::LimitExceeded(format!(
66                "VDX document exceeds {} XML events",
67                options.max_xml_events
68            )));
69        }
70        let event = reader.read_event_into(&mut buffer)?;
71        match event {
72            Event::Start(start) => {
73                let name = local_name(start.name().as_ref()).to_vec();
74                if !root_seen {
75                    if name.as_slice() != b"VisioDocument" {
76                        return Err(Error::InvalidInput(format!(
77                            "VDX root is <{}>; expected <VisioDocument>",
78                            String::from_utf8_lossy(&name)
79                        )));
80                    }
81                    root_seen = true;
82                    root_namespaces = namespace_attributes(&start);
83                }
84                depth += 1;
85                if depth > MAX_VDX_DEPTH {
86                    return Err(Error::LimitExceeded(format!(
87                        "VDX document exceeds XML nesting limit {MAX_VDX_DEPTH}"
88                    )));
89                }
90                if writer.is_none() && name.as_slice() == b"Page" {
91                    page_entry_count += 1;
92                    if page_entry_count > MAX_VDX_PAGES {
93                        return Err(Error::LimitExceeded(format!(
94                            "VDX document exceeds {MAX_VDX_PAGES} pages"
95                        )));
96                    }
97                    let parsed_page = page_from_start(&start);
98                    saw_background |=
99                        parsed_page.is_background || parsed_page.references_background;
100                    writer = Some(start_page(&root_namespaces)?);
101                    writer
102                        .as_mut()
103                        .expect("writer was initialized")
104                        .write_event(Event::Start(start.into_owned()))?;
105                    page = Some(parsed_page);
106                    page_depth = 1;
107                } else if let Some(page_writer) = writer.as_mut() {
108                    page_writer.write_event(Event::Start(start.into_owned()))?;
109                    page_depth += 1;
110                    if page_writer.get_ref().len() > MAX_VDX_PAGE_BYTES {
111                        return Err(Error::LimitExceeded(format!(
112                            "VDX page exceeds {MAX_VDX_PAGE_BYTES} bytes"
113                        )));
114                    }
115                }
116            }
117            Event::Empty(start) => {
118                let name = local_name(start.name().as_ref()).to_vec();
119                if !root_seen && name.as_slice() == b"VisioDocument" {
120                    root_seen = true;
121                    root_namespaces = namespace_attributes(&start);
122                }
123                if let Some(page_writer) = writer.as_mut() {
124                    page_writer.write_event(Event::Empty(start.into_owned()))?;
125                    if page_writer.get_ref().len() > MAX_VDX_PAGE_BYTES {
126                        return Err(Error::LimitExceeded(format!(
127                            "VDX page exceeds {MAX_VDX_PAGE_BYTES} bytes"
128                        )));
129                    }
130                } else if name.as_slice() == b"Page" {
131                    page_entry_count += 1;
132                    if page_entry_count > MAX_VDX_PAGES {
133                        return Err(Error::LimitExceeded(format!(
134                            "VDX document exceeds {MAX_VDX_PAGES} pages"
135                        )));
136                    }
137                    let page_meta = page_from_start(&start);
138                    saw_background |= page_meta.is_background || page_meta.references_background;
139                    let mut page_writer = start_page(&root_namespaces)?;
140                    page_writer.write_event(Event::Empty(start.into_owned()))?;
141                    let page_xml = finish_page(page_writer)?;
142                    render_vdx_page(
143                        &page_xml,
144                        page_meta,
145                        options,
146                        sink,
147                        &mut rendered_page_count,
148                        &mut warnings,
149                    )?;
150                }
151            }
152            Event::End(end) => {
153                depth = depth.saturating_sub(1);
154                if let Some(page_writer) = writer.as_mut() {
155                    let is_page_end = local_name(end.name().as_ref()) == b"Page" && page_depth == 1;
156                    page_writer.write_event(Event::End(end.to_owned()))?;
157                    page_depth = page_depth.saturating_sub(1);
158                    if page_writer.get_ref().len() > MAX_VDX_PAGE_BYTES {
159                        return Err(Error::LimitExceeded(format!(
160                            "VDX page exceeds {MAX_VDX_PAGE_BYTES} bytes"
161                        )));
162                    }
163                    if is_page_end {
164                        let page_writer = writer.take().expect("page writer exists");
165                        let page_xml = finish_page(page_writer)?;
166                        let page_meta = page.take().expect("page metadata exists");
167                        render_vdx_page(
168                            &page_xml,
169                            page_meta,
170                            options,
171                            sink,
172                            &mut rendered_page_count,
173                            &mut warnings,
174                        )?;
175                        page_depth = 0;
176                    }
177                }
178            }
179            Event::DocType(_) => {
180                return Err(Error::InvalidInput(
181                    "VDX document type declarations are not supported".into(),
182                ));
183            }
184            Event::Eof => break,
185            other => {
186                if let Some(page_writer) = writer.as_mut() {
187                    page_writer.write_event(other.into_owned())?;
188                    if page_writer.get_ref().len() > MAX_VDX_PAGE_BYTES {
189                        return Err(Error::LimitExceeded(format!(
190                            "VDX page exceeds {MAX_VDX_PAGE_BYTES} bytes"
191                        )));
192                    }
193                }
194            }
195        }
196        buffer.clear();
197    }
198    if !root_seen {
199        return Err(Error::InvalidInput("VDX document is empty".into()));
200    }
201    if writer.is_some() || page.is_some() {
202        return Err(Error::InvalidInput(
203            "VDX document ended inside a Page entry".into(),
204        ));
205    }
206    if rendered_page_count == 0 {
207        return Err(Error::InvalidInput(
208            "VDX document contains no foreground drawing pages".into(),
209        ));
210    }
211    if saw_background {
212        warnings
213            .push("Visio background pages are not rendered or applied to foreground pages".into());
214    }
215    Ok(warnings)
216}
217
218fn render_vdx_page(
219    page_xml: &[u8],
220    page_meta: VdxPage,
221    options: &ConvertOptions,
222    sink: &mut dyn PageConsumer,
223    rendered_page_count: &mut usize,
224    warnings: &mut Vec<String>,
225) -> Result<()> {
226    if page_meta.is_background {
227        return Ok(());
228    }
229    if *rendered_page_count >= options.max_pages.min(MAX_VDX_PAGES) {
230        return Err(Error::LimitExceeded(format!(
231            "VDX document exceeds {} output pages",
232            options.max_pages.min(MAX_VDX_PAGES)
233        )));
234    }
235    *rendered_page_count += 1;
236    let parsed = vsdx::parse_page(page_xml, options.max_xml_events, "VDX Page")?;
237    let mut output =
238        vsdx::render_page(parsed, *rendered_page_count, &page_meta.name, &page_meta.id);
239    if page_meta.name.is_empty() {
240        output.warn(format!(
241            "VDX page {} has no name; a generic title is used",
242            rendered_page_count
243        ));
244    }
245    if output.nodes.is_empty() {
246        output.warn("Visio page contains no supported visible shapes");
247    }
248    for warning in &output.warnings {
249        if !warnings.contains(warning) {
250            warnings.push(warning.clone());
251        }
252    }
253    sink.consume(output)
254}
255
256fn start_page(namespaces: &[(String, String)]) -> Result<Writer<Vec<u8>>> {
257    let mut writer = Writer::new(Vec::new());
258    let mut wrapper = BytesStart::new("PageContents");
259    for (name, value) in namespaces {
260        wrapper.push_attribute((name.as_str(), value.as_str()));
261    }
262    writer.write_event(Event::Start(wrapper))?;
263    Ok(writer)
264}
265
266fn finish_page(mut writer: Writer<Vec<u8>>) -> Result<Vec<u8>> {
267    writer.write_event(Event::End(BytesEnd::new("PageContents")))?;
268    Ok(writer.into_inner())
269}
270
271fn namespace_attributes(start: &BytesStart<'_>) -> Vec<(String, String)> {
272    start
273        .attributes()
274        .with_checks(false)
275        .flatten()
276        .filter_map(|attribute| {
277            let name = std::str::from_utf8(attribute.key.as_ref()).ok()?;
278            (name == "xmlns" || name.starts_with("xmlns:") || name == "xml:space").then(|| {
279                (
280                    name.to_owned(),
281                    String::from_utf8_lossy(attribute.value.as_ref()).into_owned(),
282                )
283            })
284        })
285        .collect()
286}
287
288fn page_from_start(start: &BytesStart<'_>) -> VdxPage {
289    let back_page = attribute(start, b"BackPage").unwrap_or_default();
290    VdxPage {
291        id: attribute(start, b"ID").unwrap_or_default(),
292        name: attribute(start, b"NameU")
293            .filter(|value| !value.is_empty())
294            .or_else(|| attribute(start, b"Name"))
295            .unwrap_or_default(),
296        is_background: attribute(start, b"IsBackground")
297            .is_some_and(|value| value == "1" || value.eq_ignore_ascii_case("true")),
298        references_background: !back_page.is_empty() && back_page != "0",
299    }
300}