#![allow(dead_code)]
use std::io::{Cursor, Write};
use zip::CompressionMethod;
use zip::write::{SimpleFileOptions, ZipWriter};
pub fn zip_bytes(entries: &[(&str, &[u8])]) -> Vec<u8> {
zip_bytes_with(entries, CompressionMethod::Deflated)
}
pub fn zip_bytes_with(entries: &[(&str, &[u8])], method: CompressionMethod) -> Vec<u8> {
let mut w = ZipWriter::new(Cursor::new(Vec::new()));
for (name, data) in entries {
let opts = if *name == "mimetype" {
SimpleFileOptions::default().compression_method(CompressionMethod::Stored)
} else {
SimpleFileOptions::default()
.compression_method(method)
.large_file(data.len() > u32::MAX as usize / 2)
};
w.start_file(*name, opts).unwrap();
w.write_all(data).unwrap();
}
w.finish().unwrap().into_inner()
}
pub const W_NS: &str = r#"xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships""#;
const DOCX_CONTENT_TYPES: &str = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
<Default Extension="xml" ContentType="application/xml"/>
<Override PartName="/word/document.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/>
</Types>"#;
const DOCX_ROOT_RELS: &str = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml"/>
<Relationship Id="rId2" Type="http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties" Target="docProps/core.xml"/>
</Relationships>"#;
#[derive(Default)]
pub struct DocxParts<'a> {
pub body: &'a str,
pub rels: &'a str,
pub styles: Option<&'a str>,
pub numbering: Option<&'a str>,
pub title: Option<&'a str>,
pub extra: Vec<(&'a str, Vec<u8>)>,
}
pub fn docx(body: &str) -> Vec<u8> {
docx_with(DocxParts {
body,
..Default::default()
})
}
pub fn docx_with(p: DocxParts<'_>) -> Vec<u8> {
let document = format!(
r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?><w:document {W_NS}><w:body>{}</w:body></w:document>"#,
p.body
);
let rels = format!(
r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">{}</Relationships>"#,
p.rels
);
let mut entries: Vec<(String, Vec<u8>)> = vec![
("[Content_Types].xml".into(), DOCX_CONTENT_TYPES.into()),
("_rels/.rels".into(), DOCX_ROOT_RELS.into()),
("word/document.xml".into(), document.into_bytes()),
("word/_rels/document.xml.rels".into(), rels.into_bytes()),
];
if let Some(styles) = p.styles {
entries.push((
"word/styles.xml".into(),
format!(r#"<?xml version="1.0"?><w:styles {W_NS}>{styles}</w:styles>"#).into_bytes(),
));
}
if let Some(numbering) = p.numbering {
entries.push((
"word/numbering.xml".into(),
format!(r#"<?xml version="1.0"?><w:numbering {W_NS}>{numbering}</w:numbering>"#)
.into_bytes(),
));
}
if let Some(title) = p.title {
entries.push((
"docProps/core.xml".into(),
format!(
r#"<?xml version="1.0"?><cp:coreProperties xmlns:cp="http://schemas.openxmlformats.org/package/2006/metadata/core-properties" xmlns:dc="http://purl.org/dc/elements/1.1/"><dc:title>{title}</dc:title></cp:coreProperties>"#
)
.into_bytes(),
));
}
for (name, data) in p.extra {
entries.push((name.to_string(), data));
}
let refs: Vec<(&str, &[u8])> = entries
.iter()
.map(|(n, d)| (n.as_str(), d.as_slice()))
.collect();
zip_bytes(&refs)
}
pub fn para(text: &str) -> String {
format!("<w:p><w:r><w:t xml:space=\"preserve\">{text}</w:t></w:r></w:p>")
}
pub fn styled_para(style: &str, text: &str) -> String {
format!("<w:p><w:pPr><w:pStyle w:val=\"{style}\"/></w:pPr><w:r><w:t>{text}</w:t></w:r></w:p>")
}
pub fn list_para(num_id: u32, ilvl: u32, text: &str) -> String {
format!(
"<w:p><w:pPr><w:pStyle w:val=\"ListParagraph\"/><w:numPr><w:ilvl w:val=\"{ilvl}\"/><w:numId w:val=\"{num_id}\"/></w:numPr></w:pPr><w:r><w:t>{text}</w:t></w:r></w:p>"
)
}
pub const NUMBERING: &str = r#"
<w:abstractNum w:abstractNumId="0">
<w:lvl w:ilvl="0"><w:start w:val="1"/><w:numFmt w:val="bullet"/></w:lvl>
<w:lvl w:ilvl="1"><w:start w:val="1"/><w:numFmt w:val="bullet"/></w:lvl>
</w:abstractNum>
<w:abstractNum w:abstractNumId="1">
<w:lvl w:ilvl="0"><w:start w:val="1"/><w:numFmt w:val="decimal"/></w:lvl>
<w:lvl w:ilvl="1"><w:start w:val="1"/><w:numFmt w:val="lowerLetter"/></w:lvl>
</w:abstractNum>
<w:num w:numId="1"><w:abstractNumId w:val="0"/></w:num>
<w:num w:numId="2"><w:abstractNumId w:val="1"/></w:num>
"#;
#[derive(Clone, Debug)]
pub enum Cell {
S(&'static str),
Str(String),
N(f64),
B(bool),
Empty,
}
pub struct Sheet {
pub name: String,
pub rows: Vec<Vec<Cell>>,
pub hidden: bool,
}
impl Sheet {
pub fn new(name: &str, rows: Vec<Vec<Cell>>) -> Self {
Self {
name: name.into(),
rows,
hidden: false,
}
}
}
pub fn col_name(mut c: usize) -> String {
let mut s = Vec::new();
loop {
s.push(b'A' + (c % 26) as u8);
if c < 26 {
break;
}
c = c / 26 - 1;
}
s.reverse();
String::from_utf8(s).unwrap()
}
pub fn xml_escape(s: &str) -> String {
s.replace('&', "&")
.replace('<', "<")
.replace('>', ">")
.replace('"', """)
}
fn cell_xml(r: usize, c: usize, cell: &Cell) -> String {
let at = format!("{}{}", col_name(c), r + 1);
match cell {
Cell::S(s) => format!(
r#"<c r="{at}" t="inlineStr"><is><t xml:space="preserve">{}</t></is></c>"#,
xml_escape(s)
),
Cell::Str(s) => format!(
r#"<c r="{at}" t="inlineStr"><is><t xml:space="preserve">{}</t></is></c>"#,
xml_escape(s)
),
Cell::N(n) => format!(r#"<c r="{at}"><v>{n}</v></c>"#),
Cell::B(b) => format!(r#"<c r="{at}" t="b"><v>{}</v></c>"#, u8::from(*b)),
Cell::Empty => String::new(),
}
}
pub fn worksheet_xml(sheet_data: &str) -> String {
format!(
r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?><worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"><sheetData>{sheet_data}</sheetData></worksheet>"#
)
}
pub fn sheet_data(rows: &[Vec<Cell>]) -> String {
let mut out = String::new();
for (r, row) in rows.iter().enumerate() {
out.push_str(&format!(r#"<row r="{}">"#, r + 1));
for (c, cell) in row.iter().enumerate() {
out.push_str(&cell_xml(r, c, cell));
}
out.push_str("</row>");
}
out
}
pub fn xlsx_with(
sheets: &[Sheet],
raw_sheet_xml: &[Option<String>],
extra: &[(&str, &[u8])],
) -> Vec<u8> {
let mut ct = String::from(
r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/><Default Extension="xml" ContentType="application/xml"/><Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/>"#,
);
let mut wb_sheets = String::new();
let mut wb_rels = String::new();
let mut entries: Vec<(String, Vec<u8>)> = Vec::new();
for (i, s) in sheets.iter().enumerate() {
let n = i + 1;
ct.push_str(&format!(r#"<Override PartName="/xl/worksheets/sheet{n}.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>"#));
let state = if s.hidden { r#" state="hidden""# } else { "" };
wb_sheets.push_str(&format!(
r#"<sheet name="{}" sheetId="{n}"{state} r:id="rId{n}"/>"#,
xml_escape(&s.name)
));
wb_rels.push_str(&format!(r#"<Relationship Id="rId{n}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet{n}.xml"/>"#));
let xml = raw_sheet_xml
.get(i)
.cloned()
.flatten()
.unwrap_or_else(|| worksheet_xml(&sheet_data(&s.rows)));
entries.push((format!("xl/worksheets/sheet{n}.xml"), xml.into_bytes()));
}
ct.push_str("</Types>");
let workbook = format!(
r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?><workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"><sheets>{wb_sheets}</sheets></workbook>"#
);
let rels = format!(
r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">{wb_rels}</Relationships>"#
);
let root_rels = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="xl/workbook.xml"/></Relationships>"#;
let mut all: Vec<(String, Vec<u8>)> = vec![
("[Content_Types].xml".into(), ct.into_bytes()),
("_rels/.rels".into(), root_rels.as_bytes().to_vec()),
("xl/workbook.xml".into(), workbook.into_bytes()),
("xl/_rels/workbook.xml.rels".into(), rels.into_bytes()),
];
all.extend(entries);
for (n, d) in extra {
all.push((n.to_string(), d.to_vec()));
}
let refs: Vec<(&str, &[u8])> = all
.iter()
.map(|(n, d)| (n.as_str(), d.as_slice()))
.collect();
zip_bytes(&refs)
}
pub fn xlsx(sheets: &[Sheet]) -> Vec<u8> {
xlsx_with(sheets, &[], &[])
}
pub const ODS_MIME: &str = "application/vnd.oasis.opendocument.spreadsheet";
pub fn ods(spreadsheet_inner: &str) -> Vec<u8> {
let content = format!(
r#"<?xml version="1.0" encoding="UTF-8"?><office:document-content xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0" xmlns:table="urn:oasis:names:tc:opendocument:xmlns:table:1.0" xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0" office:version="1.2"><office:body><office:spreadsheet>{spreadsheet_inner}</office:spreadsheet></office:body></office:document-content>"#
);
let manifest = r#"<?xml version="1.0" encoding="UTF-8"?><manifest:manifest xmlns:manifest="urn:oasis:names:tc:opendocument:xmlns:manifest:1.0"><manifest:file-entry manifest:full-path="/" manifest:media-type="application/vnd.oasis.opendocument.spreadsheet"/><manifest:file-entry manifest:full-path="content.xml" manifest:media-type="text/xml"/></manifest:manifest>"#;
zip_bytes(&[
("mimetype", ODS_MIME.as_bytes()),
("content.xml", content.as_bytes()),
("META-INF/manifest.xml", manifest.as_bytes()),
])
}
pub fn ods_string_cell(s: &str) -> String {
format!(
r#"<table:table-cell office:value-type="string"><text:p>{}</text:p></table:table-cell>"#,
xml_escape(s)
)
}
pub fn ods_float_cell(v: f64) -> String {
format!(
r#"<table:table-cell office:value-type="float" office:value="{v}"><text:p>{v}</text:p></table:table-cell>"#
)
}
fn record(out: &mut Vec<u8>, typ: u16, data: &[u8]) {
out.extend_from_slice(&typ.to_le_bytes());
out.extend_from_slice(&(data.len() as u16).to_le_bytes());
out.extend_from_slice(data);
}
fn bof(dt: u16) -> Vec<u8> {
let mut d = Vec::new();
d.extend_from_slice(&0x0600u16.to_le_bytes()); d.extend_from_slice(&dt.to_le_bytes());
d.extend_from_slice(&0x0DBBu16.to_le_bytes());
d.extend_from_slice(&0x07CCu16.to_le_bytes());
d.extend_from_slice(&0u32.to_le_bytes());
d.extend_from_slice(&0x0006u32.to_le_bytes());
d
}
pub enum XlsCell {
Num(u16, u16, f64),
Label(u16, u16, &'static str),
}
pub struct XlsSheet {
pub name: &'static str,
pub dims: Option<(u32, u32, u16, u16)>,
pub cells: Vec<XlsCell>,
}
pub fn biff8_workbook(sheets: &[XlsSheet]) -> Vec<u8> {
let mut globals = Vec::new();
record(&mut globals, 0x0809, &bof(0x0005));
record(&mut globals, 0x0042, &1200u16.to_le_bytes());
let mut patch_at = Vec::new();
for s in sheets {
let mut d = Vec::new();
d.extend_from_slice(&0u32.to_le_bytes()); d.push(0); d.push(0); d.push(s.name.len() as u8);
d.push(0); d.extend_from_slice(s.name.as_bytes());
patch_at.push(globals.len() + 4);
record(&mut globals, 0x0085, &d);
}
record(&mut globals, 0x000A, &[]);
let mut stream = globals;
for (i, s) in sheets.iter().enumerate() {
let pos = stream.len() as u32;
stream[patch_at[i]..patch_at[i] + 4].copy_from_slice(&pos.to_le_bytes());
record(&mut stream, 0x0809, &bof(0x0010));
if let Some((rf, rl, cf, cl)) = s.dims {
let mut d = Vec::new();
d.extend_from_slice(&rf.to_le_bytes());
d.extend_from_slice(&rl.to_le_bytes());
d.extend_from_slice(&cf.to_le_bytes());
d.extend_from_slice(&cl.to_le_bytes());
d.extend_from_slice(&0u16.to_le_bytes());
record(&mut stream, 0x0200, &d);
}
for c in &s.cells {
match c {
XlsCell::Num(r, col, v) => {
let mut d = Vec::new();
d.extend_from_slice(&r.to_le_bytes());
d.extend_from_slice(&col.to_le_bytes());
d.extend_from_slice(&0u16.to_le_bytes());
d.extend_from_slice(&v.to_le_bytes());
record(&mut stream, 0x0203, &d);
}
XlsCell::Label(r, col, text) => {
let mut d = Vec::new();
d.extend_from_slice(&r.to_le_bytes());
d.extend_from_slice(&col.to_le_bytes());
d.extend_from_slice(&0u16.to_le_bytes());
d.extend_from_slice(&(text.len() as u16).to_le_bytes());
d.push(0);
d.extend_from_slice(text.as_bytes());
record(&mut stream, 0x0204, &d);
}
}
}
record(&mut stream, 0x000A, &[]);
}
stream
}
pub fn xls_from_stream(stream: &[u8]) -> Vec<u8> {
let mut comp = cfb::CompoundFile::create(Cursor::new(Vec::new())).unwrap();
{
let mut s = comp.create_stream("/Workbook").unwrap();
s.write_all(stream).unwrap();
}
comp.flush().unwrap();
comp.into_inner().into_inner()
}
pub fn xls(sheets: &[XlsSheet]) -> Vec<u8> {
xls_from_stream(&biff8_workbook(sheets))
}