office2pdf 0.6.2

Convert DOCX, XLSX, and PPTX files to PDF using pure Rust
Documentation
use std::fmt::Write;

const PLACEABLE_KEY: u32 = 0x9AC6_CDD7;
const META_EOF: u16 = 0x0000;
const META_SET_POLY_FILL_MODE: u16 = 0x0106;
const META_SELECT_OBJECT: u16 = 0x012D;
const META_DELETE_OBJECT: u16 = 0x01F0;
const META_SET_WINDOW_EXT: u16 = 0x020C;
const META_CREATE_PEN_INDIRECT: u16 = 0x02FA;
const META_CREATE_BRUSH_INDIRECT: u16 = 0x02FC;
const META_POLYGON: u16 = 0x0324;

const BS_SOLID: u16 = 0;
const BS_NULL: u16 = 1;
const PS_NULL: u16 = 5;

#[derive(Clone, Copy)]
struct RgbColor {
    red: u8,
    green: u8,
    blue: u8,
}

impl RgbColor {
    fn from_colorref(colorref: u32) -> Self {
        Self {
            red: (colorref & 0xff) as u8,
            green: ((colorref >> 8) & 0xff) as u8,
            blue: ((colorref >> 16) & 0xff) as u8,
        }
    }

    fn as_svg_hex(self) -> String {
        format!("#{:02x}{:02x}{:02x}", self.red, self.green, self.blue)
    }
}

#[derive(Clone, Copy)]
enum WmfObject {
    Brush(Option<RgbColor>),
    Pen { color: Option<RgbColor>, width: i16 },
}

struct WmfSvgConverter {
    objects: Vec<Option<WmfObject>>,
    brush: Option<RgbColor>,
    pen: Option<RgbColor>,
    pen_width: i16,
    uses_even_odd_fill: bool,
    has_inverted_y_axis: bool,
    elements: String,
}

impl WmfSvgConverter {
    fn new() -> Self {
        Self {
            objects: Vec::new(),
            brush: None,
            pen: None,
            pen_width: 1,
            uses_even_odd_fill: true,
            has_inverted_y_axis: false,
            elements: String::new(),
        }
    }

    fn insert_object(&mut self, object: WmfObject) {
        if let Some(slot) = self.objects.iter_mut().find(|slot| slot.is_none()) {
            *slot = Some(object);
        } else {
            self.objects.push(Some(object));
        }
    }

    fn handle_record(&mut self, record_type: u16, parameters: &[u8]) -> Option<()> {
        match record_type {
            META_CREATE_BRUSH_INDIRECT => {
                let style: u16 = read_u16(parameters, 0)?;
                let color: Option<RgbColor> = if style == BS_SOLID {
                    Some(RgbColor::from_colorref(read_u32(parameters, 2)?))
                } else {
                    None
                };
                self.insert_object(WmfObject::Brush(if style == BS_NULL {
                    None
                } else {
                    color
                }));
            }
            META_CREATE_PEN_INDIRECT => {
                let style: u16 = read_u16(parameters, 0)? & 0x000f;
                let width: i16 = read_i16(parameters, 2)?.unsigned_abs().max(1) as i16;
                let color: Option<RgbColor> = if style != PS_NULL {
                    Some(RgbColor::from_colorref(read_u32(parameters, 6)?))
                } else {
                    None
                };
                self.insert_object(WmfObject::Pen { color, width });
            }
            META_SELECT_OBJECT => {
                let index: usize = read_u16(parameters, 0)? as usize;
                match self.objects.get(index).copied().flatten()? {
                    WmfObject::Brush(color) => self.brush = color,
                    WmfObject::Pen { color, width } => {
                        self.pen = color;
                        self.pen_width = width;
                    }
                }
            }
            META_DELETE_OBJECT => {
                let index: usize = read_u16(parameters, 0)? as usize;
                if let Some(slot) = self.objects.get_mut(index) {
                    *slot = None;
                }
            }
            META_SET_POLY_FILL_MODE => {
                self.uses_even_odd_fill = read_u16(parameters, 0)? == 1;
            }
            META_SET_WINDOW_EXT => {
                self.has_inverted_y_axis = read_i16(parameters, 0)? < 0;
            }
            META_POLYGON => self.write_polygon(parameters)?,
            _ => {}
        }
        Some(())
    }

    fn write_polygon(&mut self, parameters: &[u8]) -> Option<()> {
        let point_count: usize = read_u16(parameters, 0)? as usize;
        if point_count < 2 || 2usize.checked_add(point_count.checked_mul(4)?)? > parameters.len() {
            return None;
        }

        self.elements.push_str("<path d=\"M");
        for index in 0..point_count {
            let offset: usize = 2 + index * 4;
            let x: i16 = read_i16(parameters, offset)?;
            let y: i16 = read_i16(parameters, offset + 2)?;
            if index > 0 {
                self.elements.push(' ');
            }
            write!(self.elements, "{x} {y}").ok()?;
        }
        self.elements.push_str("Z\"");
        match self.brush {
            Some(color) => write!(self.elements, " fill=\"{}\"", color.as_svg_hex()).ok()?,
            None => self.elements.push_str(" fill=\"none\""),
        }
        match self.pen {
            Some(color) => write!(
                self.elements,
                " stroke=\"{}\" stroke-width=\"{}\"",
                color.as_svg_hex(),
                self.pen_width
            )
            .ok()?,
            None => self.elements.push_str(" stroke=\"none\""),
        }
        if self.uses_even_odd_fill {
            self.elements.push_str(" fill-rule=\"evenodd\"");
        }
        self.elements.push_str("/>\n");
        Some(())
    }
}

pub(super) fn convert_wmf_to_svg(data: &[u8]) -> Option<Vec<u8>> {
    if read_u32(data, 0)? != PLACEABLE_KEY {
        return None;
    }
    let left: i16 = read_i16(data, 6)?;
    let top: i16 = read_i16(data, 8)?;
    let right: i16 = read_i16(data, 10)?;
    let bottom: i16 = read_i16(data, 12)?;
    let width: i32 = i32::from(right) - i32::from(left);
    let height: i32 = i32::from(bottom) - i32::from(top);
    if width <= 0 || height <= 0 || read_u16(data, 24)? != 9 {
        return None;
    }

    let mut converter: WmfSvgConverter = WmfSvgConverter::new();
    let mut offset: usize = 40;
    let mut saw_eof: bool = false;
    while offset.checked_add(6)? <= data.len() {
        let record_words: usize = read_u32(data, offset)? as usize;
        let record_size: usize = record_words.checked_mul(2)?;
        if record_size < 6 || offset.checked_add(record_size)? > data.len() {
            return None;
        }
        let record_type: u16 = read_u16(data, offset + 4)?;
        converter.handle_record(record_type, &data[offset + 6..offset + record_size])?;
        offset += record_size;
        if record_type == META_EOF {
            saw_eof = true;
            break;
        }
    }
    if !saw_eof || converter.elements.is_empty() {
        return None;
    }

    let elements: String = if converter.has_inverted_y_axis {
        let vertical_translation: i32 = i32::from(top) + i32::from(bottom);
        format!(
            "<g transform=\"translate(0 {vertical_translation}) scale(1 -1)\">\n{}</g>\n",
            converter.elements
        )
    } else {
        converter.elements
    };
    Some(
        format!(
            "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"{left} {top} {width} {height}\">\n{elements}</svg>"
        )
        .into_bytes(),
    )
}

fn read_u16(data: &[u8], offset: usize) -> Option<u16> {
    Some(u16::from_le_bytes(
        data.get(offset..offset + 2)?.try_into().ok()?,
    ))
}

fn read_i16(data: &[u8], offset: usize) -> Option<i16> {
    Some(i16::from_le_bytes(
        data.get(offset..offset + 2)?.try_into().ok()?,
    ))
}

fn read_u32(data: &[u8], offset: usize) -> Option<u32> {
    Some(u32::from_le_bytes(
        data.get(offset..offset + 4)?.try_into().ok()?,
    ))
}

#[cfg(test)]
mod tests {
    use super::*;

    fn record(record_type: u16, parameters: &[u8]) -> Vec<u8> {
        let mut bytes: Vec<u8> = Vec::new();
        bytes.extend_from_slice(&((parameters.len() + 6) as u32 / 2).to_le_bytes());
        bytes.extend_from_slice(&record_type.to_le_bytes());
        bytes.extend_from_slice(parameters);
        bytes
    }

    fn simple_wmf(colorref: u32, points: &[(i16, i16)]) -> Vec<u8> {
        let mut records: Vec<u8> = Vec::new();
        let mut window_extent: Vec<u8> = Vec::new();
        window_extent.extend_from_slice(&(-100i16).to_le_bytes());
        window_extent.extend_from_slice(&100i16.to_le_bytes());
        records.extend(record(META_SET_WINDOW_EXT, &window_extent));
        let mut brush: Vec<u8> = Vec::new();
        brush.extend_from_slice(&BS_SOLID.to_le_bytes());
        brush.extend_from_slice(&colorref.to_le_bytes());
        brush.extend_from_slice(&0u16.to_le_bytes());
        records.extend(record(META_CREATE_BRUSH_INDIRECT, &brush));
        records.extend(record(META_SELECT_OBJECT, &0u16.to_le_bytes()));
        let mut polygon: Vec<u8> = Vec::new();
        polygon.extend_from_slice(&(points.len() as u16).to_le_bytes());
        for (x, y) in points {
            polygon.extend_from_slice(&x.to_le_bytes());
            polygon.extend_from_slice(&y.to_le_bytes());
        }
        records.extend(record(META_POLYGON, &polygon));
        records.extend(record(META_EOF, &[]));

        let mut data: Vec<u8> = Vec::new();
        data.extend_from_slice(&PLACEABLE_KEY.to_le_bytes());
        data.extend_from_slice(&0u16.to_le_bytes());
        data.extend_from_slice(&(-10i16).to_le_bytes());
        data.extend_from_slice(&(-20i16).to_le_bytes());
        data.extend_from_slice(&90i16.to_le_bytes());
        data.extend_from_slice(&80i16.to_le_bytes());
        data.extend_from_slice(&1440u16.to_le_bytes());
        data.extend_from_slice(&0u32.to_le_bytes());
        data.extend_from_slice(&0u16.to_le_bytes());
        data.extend_from_slice(&1u16.to_le_bytes());
        data.extend_from_slice(&9u16.to_le_bytes());
        data.extend_from_slice(&0x0300u16.to_le_bytes());
        data.extend_from_slice(&((18 + records.len()) as u32 / 2).to_le_bytes());
        data.extend_from_slice(&1u16.to_le_bytes());
        data.extend_from_slice(&0u32.to_le_bytes());
        data.extend_from_slice(&0u16.to_le_bytes());
        data.extend(records);
        data
    }

    #[test]
    fn converts_placeable_wmf_polygon_to_svg() {
        let data: Vec<u8> = simple_wmf(0x0033_2211, &[(0, 0), (30, 0), (15, 40)]);
        let svg: String = String::from_utf8(convert_wmf_to_svg(&data).unwrap()).unwrap();

        assert!(svg.contains("viewBox=\"-10 -20 100 100\""));
        assert!(svg.contains("transform=\"translate(0 60) scale(1 -1)\""));
        assert!(svg.contains("M0 0 30 0 15 40Z"));
        assert!(svg.contains("fill=\"#112233\""));
    }

    #[test]
    fn converts_different_polygon_without_fixture_specific_assumptions() {
        let data: Vec<u8> = simple_wmf(0x0000_80ff, &[(-5, 6), (7, 8), (9, -10), (2, 1)]);
        let svg: String = String::from_utf8(convert_wmf_to_svg(&data).unwrap()).unwrap();

        assert!(svg.contains("M-5 6 7 8 9 -10 2 1Z"));
        assert!(svg.contains("fill=\"#ff8000\""));
    }

    #[test]
    fn rejects_non_wmf_data() {
        assert!(convert_wmf_to_svg(b"not a metafile").is_none());
    }
}