use std::collections::BTreeMap;
use lo_calc::{evaluate_formula, Value};
use lo_core::{
parse_xml_document, serialize_xml_document, CellAddr, LoError, Result, Sheet, XmlItem, XmlNode,
};
use lo_zip::{normalize_zip_path, rels_path_for, resolve_part_target, ZipArchive};
pub fn docx_to_pdf_bytes(bytes: &[u8]) -> Result<Vec<u8>> {
let doc = lo_writer::from_docx_bytes("document", bytes)?;
lo_writer::save_as(&doc, "pdf")
}
pub fn doc_to_docx_bytes(bytes: &[u8]) -> Result<Vec<u8>> {
let doc = lo_writer::from_doc_bytes("document", bytes)?;
lo_writer::save_as(&doc, "docx")
}
pub fn pptx_to_pdf_bytes(bytes: &[u8]) -> Result<Vec<u8>> {
let deck = lo_impress::from_pptx_bytes("presentation", bytes)?;
lo_impress::save_as(&deck, "pdf")
}
pub fn writer_convert_bytes(input: &[u8], from: &str, to: &str) -> Result<Vec<u8>> {
let doc = lo_writer::load_bytes("document", input, from)?;
lo_writer::save_as(&doc, to)
}
pub fn calc_convert_bytes(input: &[u8], from: &str, to: &str) -> Result<Vec<u8>> {
let workbook = lo_calc::load_bytes("workbook", input, from)?;
lo_calc::save_as(&workbook, to)
}
pub fn impress_convert_bytes(input: &[u8], from: &str, to: &str) -> Result<Vec<u8>> {
let deck = lo_impress::load_bytes("presentation", input, from)?;
lo_impress::save_as(&deck, to)
}
pub fn docx_to_html_bytes(input: &[u8]) -> Result<Vec<u8>> {
writer_convert_bytes(input, "docx", "html")
}
pub fn docx_to_txt_bytes(input: &[u8]) -> Result<Vec<u8>> {
writer_convert_bytes(input, "docx", "txt")
}
pub fn docx_to_odt_bytes(input: &[u8]) -> Result<Vec<u8>> {
writer_convert_bytes(input, "docx", "odt")
}
pub fn odt_to_pdf_bytes(input: &[u8]) -> Result<Vec<u8>> {
writer_convert_bytes(input, "odt", "pdf")
}
pub fn odt_to_docx_bytes(input: &[u8]) -> Result<Vec<u8>> {
writer_convert_bytes(input, "odt", "docx")
}
pub fn odt_to_html_bytes(input: &[u8]) -> Result<Vec<u8>> {
writer_convert_bytes(input, "odt", "html")
}
pub fn xlsx_to_pdf_bytes(input: &[u8]) -> Result<Vec<u8>> {
calc_convert_bytes(input, "xlsx", "pdf")
}
pub fn xlsx_to_html_bytes(input: &[u8]) -> Result<Vec<u8>> {
calc_convert_bytes(input, "xlsx", "html")
}
pub fn xlsx_to_csv_bytes(input: &[u8]) -> Result<Vec<u8>> {
calc_convert_bytes(input, "xlsx", "csv")
}
pub fn xlsx_to_ods_bytes(input: &[u8]) -> Result<Vec<u8>> {
calc_convert_bytes(input, "xlsx", "ods")
}
pub fn ods_to_pdf_bytes(input: &[u8]) -> Result<Vec<u8>> {
calc_convert_bytes(input, "ods", "pdf")
}
pub fn ods_to_xlsx_bytes(input: &[u8]) -> Result<Vec<u8>> {
calc_convert_bytes(input, "ods", "xlsx")
}
pub fn ods_to_csv_bytes(input: &[u8]) -> Result<Vec<u8>> {
calc_convert_bytes(input, "ods", "csv")
}
pub fn pptx_to_html_bytes(input: &[u8]) -> Result<Vec<u8>> {
impress_convert_bytes(input, "pptx", "html")
}
pub fn pptx_to_svg_bytes(input: &[u8]) -> Result<Vec<u8>> {
impress_convert_bytes(input, "pptx", "svg")
}
pub fn pptx_to_odp_bytes(input: &[u8]) -> Result<Vec<u8>> {
impress_convert_bytes(input, "pptx", "odp")
}
pub fn odp_to_pdf_bytes(input: &[u8]) -> Result<Vec<u8>> {
impress_convert_bytes(input, "odp", "pdf")
}
pub fn odp_to_pptx_bytes(input: &[u8]) -> Result<Vec<u8>> {
impress_convert_bytes(input, "odp", "pptx")
}
pub fn xlsx_recalc_bytes(bytes: &[u8]) -> Result<Vec<u8>> {
let zip = ZipArchive::new(bytes)?;
let workbook = lo_calc::from_xlsx_bytes("workbook", bytes)?;
let sheet_targets = parse_xlsx_sheet_targets(&zip)?;
let mut entries: Vec<lo_zip::ZipEntry> = Vec::new();
for entry_name in zip.entries() {
let path = normalize_zip_path(entry_name);
if path == "xl/calcChain.xml" {
continue;
}
if path == "[Content_Types].xml" {
let xml = zip.read_string(&path)?;
let mut root = parse_xml_document(&xml)?;
remove_content_type_override(&mut root, "/xl/calcChain.xml");
entries.push(lo_zip::ZipEntry::new(
path,
serialize_xml_document(&root).into_bytes(),
));
continue;
}
if path == "xl/_rels/workbook.xml.rels" {
let xml = zip.read_string(&path)?;
let mut root = parse_xml_document(&xml)?;
remove_calc_chain_relationships(&mut root);
entries.push(lo_zip::ZipEntry::new(
path,
serialize_xml_document(&root).into_bytes(),
));
continue;
}
if path == "xl/workbook.xml" {
let xml = zip.read_string(&path)?;
let mut root = parse_xml_document(&xml)?;
mark_workbook_recalculated(&mut root);
entries.push(lo_zip::ZipEntry::new(
path,
serialize_xml_document(&root).into_bytes(),
));
continue;
}
if let Some(sheet_index) = sheet_targets.iter().position(|(target, _)| target == &path) {
if let Some(sheet) = workbook.sheets.get(sheet_index) {
let xml = zip.read_string(&path)?;
let mut root = parse_xml_document(&xml)?;
patch_xlsx_sheet_formula_cache(&mut root, sheet)?;
entries.push(lo_zip::ZipEntry::new(
path,
serialize_xml_document(&root).into_bytes(),
));
continue;
}
}
entries.push(lo_zip::ZipEntry::new(path, zip.read(entry_name)?));
}
lo_zip::ooxml_package(&entries)
}
fn parse_xlsx_sheet_targets(zip: &ZipArchive) -> Result<Vec<(String, String)>> {
let workbook_root = parse_xml_document(&zip.read_string("xl/workbook.xml")?)?;
let rels = parse_relationships(zip, "xl/workbook.xml")?;
let mut out = Vec::new();
if let Some(sheets) = workbook_root.child("sheets") {
for (index, sheet) in sheets.children_named("sheet").enumerate() {
let name = sheet.attr("name").unwrap_or("Sheet").to_string();
let target = sheet
.attr("id")
.or_else(|| sheet.attr("r:id"))
.and_then(|id| rels.get(id))
.cloned()
.unwrap_or_else(|| format!("xl/worksheets/sheet{}.xml", index + 1));
out.push((normalize_zip_path(&target), name));
}
}
Ok(out)
}
fn parse_relationships(zip: &ZipArchive, part: &str) -> Result<BTreeMap<String, String>> {
let rels_path = rels_path_for(part);
if !zip.contains(&rels_path) {
return Ok(BTreeMap::new());
}
let root = parse_xml_document(&zip.read_string(&rels_path)?)?;
let mut map = BTreeMap::new();
for rel in root.children_named("Relationship") {
if let (Some(id), Some(target)) = (rel.attr("Id"), rel.attr("Target")) {
map.insert(id.to_string(), resolve_part_target(part, target));
}
}
Ok(map)
}
fn remove_content_type_override(root: &mut XmlNode, part_name: &str) {
root.items.retain(|item| match item {
XmlItem::Node(node) if node.local_name() == "Override" => {
node.attr("PartName") != Some(part_name)
}
_ => true,
});
sync_node_children(root);
}
fn remove_calc_chain_relationships(root: &mut XmlNode) {
root.items.retain(|item| match item {
XmlItem::Node(node) if node.local_name() == "Relationship" => {
let target = node.attr("Target").unwrap_or("");
let rel_type = node.attr("Type").unwrap_or("");
!target.ends_with("calcChain.xml")
&& !rel_type.to_ascii_lowercase().contains("calcchain")
}
_ => true,
});
sync_node_children(root);
}
fn mark_workbook_recalculated(root: &mut XmlNode) {
let mut found = false;
for item in &mut root.items {
if let XmlItem::Node(node) = item {
if node.local_name() == "calcPr" {
node.attributes
.insert("calcCompleted".to_string(), "1".to_string());
node.attributes
.insert("fullCalcOnLoad".to_string(), "0".to_string());
node.attributes.remove("calcMode");
found = true;
}
}
}
if !found {
let mut attrs = BTreeMap::new();
attrs.insert("calcCompleted".to_string(), "1".to_string());
attrs.insert("fullCalcOnLoad".to_string(), "0".to_string());
root.items.push(XmlItem::Node(XmlNode {
name: "calcPr".to_string(),
attributes: attrs,
children: Vec::new(),
items: Vec::new(),
text: String::new(),
}));
}
sync_node_children(root);
}
fn patch_xlsx_sheet_formula_cache(root: &mut XmlNode, sheet: &Sheet) -> Result<()> {
let Some(sheet_data) = child_mut(root, "sheetData") else {
return Ok(());
};
for row in &mut sheet_data.children {
if row.local_name() != "row" {
continue;
}
let row_number = row
.attr("r")
.and_then(|value| value.parse::<usize>().ok())
.unwrap_or(1);
for cell in &mut row.children {
if cell.local_name() == "c" {
patch_formula_cell(cell, row_number, sheet)?;
}
}
sync_node_items_from_children(row);
}
sync_node_items_from_children(sheet_data);
sync_node_items_from_children(root);
Ok(())
}
fn patch_formula_cell(cell: &mut XmlNode, fallback_row: usize, sheet: &Sheet) -> Result<()> {
let formula = cell
.children
.iter()
.find(|child| child.local_name() == "f")
.map(|node| text_content(node));
let Some(formula) = formula else {
return Ok(());
};
if formula.trim().is_empty() {
return Ok(());
}
let (row_1, col_1) = cell
.attr("r")
.and_then(parse_a1_cell_ref)
.unwrap_or((fallback_row, 1));
let _addr = CellAddr::new(
row_1.saturating_sub(1) as u32,
col_1.saturating_sub(1) as u32,
);
let value = evaluate_formula(&formula, sheet)?;
let mut new_items = Vec::new();
for item in &cell.items {
match item {
XmlItem::Text(text) => new_items.push(XmlItem::Text(text.clone())),
XmlItem::Node(node) if matches!(node.local_name(), "v" | "is") => {}
XmlItem::Node(node) => new_items.push(XmlItem::Node(node.clone())),
}
}
new_items.push(XmlItem::Node(make_value_node(&value)));
cell.items = new_items;
sync_node_children(cell);
apply_formula_cache_type(cell, &value);
Ok(())
}
fn text_content(node: &XmlNode) -> String {
let mut out = String::new();
if !node.text.is_empty() {
out.push_str(&node.text);
}
for child in &node.children {
out.push_str(&text_content(child));
}
out
}
fn apply_formula_cache_type(cell: &mut XmlNode, value: &Value) {
match value {
Value::Number(_) | Value::Blank => {
cell.attributes.remove("t");
}
Value::Text(_) => {
cell.attributes.insert("t".to_string(), "str".to_string());
}
Value::Bool(_) => {
cell.attributes.insert("t".to_string(), "b".to_string());
}
Value::Error(_) => {
cell.attributes.insert("t".to_string(), "e".to_string());
}
}
}
fn make_value_node(value: &Value) -> XmlNode {
let text = match value {
Value::Blank => String::new(),
Value::Number(number) => {
if number.fract() == 0.0 && number.is_finite() {
format!("{}", *number as i64)
} else {
number.to_string()
}
}
Value::Text(text) => text.clone(),
Value::Bool(value) => {
if *value {
"1".to_string()
} else {
"0".to_string()
}
}
Value::Error(text) => text.clone(),
};
XmlNode {
name: "v".to_string(),
attributes: BTreeMap::new(),
children: Vec::new(),
items: if text.is_empty() {
Vec::new()
} else {
vec![XmlItem::Text(text.clone())]
},
text,
}
}
fn parse_a1_cell_ref(input: &str) -> Option<(usize, usize)> {
let mut letters = String::new();
let mut digits = String::new();
for ch in input.chars() {
if ch == '$' {
continue;
}
if ch.is_ascii_alphabetic() && digits.is_empty() {
letters.push(ch);
} else if ch.is_ascii_digit() {
digits.push(ch);
} else {
return None;
}
}
if letters.is_empty() || digits.is_empty() {
return None;
}
let row = digits.parse().ok()?;
let mut col = 0usize;
for ch in letters.chars() {
col = col * 26 + ((ch.to_ascii_uppercase() as u8 - b'A' + 1) as usize);
}
Some((row, col))
}
pub fn accept_all_tracked_changes_docx_bytes(bytes: &[u8]) -> Result<Vec<u8>> {
let zip = ZipArchive::new(bytes)?;
let mut entries: Vec<lo_zip::ZipEntry> = Vec::new();
for entry_name in zip.entries() {
let path = normalize_zip_path(entry_name);
if is_wordprocessing_xml(&path) {
let xml = zip.read_string(&path)?;
let root = parse_xml_document(&xml)?;
let accepted = accept_revision_root(&root, &path);
entries.push(lo_zip::ZipEntry::new(
path,
serialize_xml_document(&accepted).into_bytes(),
));
} else {
entries.push(lo_zip::ZipEntry::new(path, zip.read(entry_name)?));
}
}
lo_zip::ooxml_package(&entries)
}
pub fn accept_tracked_changes_docx_bytes(bytes: &[u8]) -> Result<Vec<u8>> {
accept_all_tracked_changes_docx_bytes(bytes)
}
pub fn recalc_existing_xlsx_bytes(bytes: &[u8]) -> Result<Vec<u8>> {
xlsx_recalc_bytes(bytes)
}
fn is_wordprocessing_xml(path: &str) -> bool {
path.starts_with("word/")
&& path.ends_with(".xml")
&& !path.contains("_rels/")
&& !path.ends_with("fontTable.xml")
}
fn accept_revision_root(root: &XmlNode, path: &str) -> XmlNode {
let items = accept_revision_items(&root.items);
let mut node = rebuild_node(root, items, root.attributes.clone());
if path.ends_with("settings.xml") {
node.items.retain(
|item| !matches!(item, XmlItem::Node(child) if child.local_name() == "trackRevisions"),
);
sync_node_children(&mut node);
}
node
}
fn accept_revision_items(items: &[XmlItem]) -> Vec<XmlItem> {
let mut out = Vec::new();
for item in items {
match item {
XmlItem::Text(text) => out.push(XmlItem::Text(text.clone())),
XmlItem::Node(node) => out.extend(accept_revision_node(node)),
}
}
out
}
fn accept_revision_node(node: &XmlNode) -> Vec<XmlItem> {
let local = node.local_name();
if matches!(
local,
"del"
| "delText"
| "delInstrText"
| "cellDel"
| "moveFrom"
| "moveFromRangeStart"
| "moveFromRangeEnd"
| "moveToRangeStart"
| "moveToRangeEnd"
| "customXmlDelRangeStart"
| "customXmlDelRangeEnd"
| "customXmlMoveFromRangeStart"
| "customXmlMoveFromRangeEnd"
| "customXmlMoveToRangeStart"
| "customXmlMoveToRangeEnd"
| "trackRevisions"
) {
return Vec::new();
}
if matches!(
local,
"ins" | "moveTo" | "customXmlInsRangeStart" | "customXmlInsRangeEnd"
) {
return accept_revision_items(&node.items);
}
if local.ends_with("Change") {
return Vec::new();
}
if row_deleted(node) {
return Vec::new();
}
let items = accept_revision_items(&node.items);
vec![XmlItem::Node(rebuild_node(
node,
items,
node.attributes.clone(),
))]
}
fn row_deleted(node: &XmlNode) -> bool {
if node.local_name() != "tr" {
return false;
}
node.child("trPr")
.map(|trpr| {
trpr.children
.iter()
.any(|child| child.local_name() == "del")
})
.unwrap_or(false)
}
fn rebuild_node(
template: &XmlNode,
items: Vec<XmlItem>,
attributes: BTreeMap<String, String>,
) -> XmlNode {
let mut node = XmlNode {
name: template.name.clone(),
attributes,
children: Vec::new(),
items,
text: String::new(),
};
sync_node_children(&mut node);
node
}
fn sync_node_children(node: &mut XmlNode) {
node.children = node
.items
.iter()
.filter_map(|item| match item {
XmlItem::Node(child) => Some(child.clone()),
_ => None,
})
.collect();
node.text = node
.items
.iter()
.filter_map(|item| match item {
XmlItem::Text(text) => Some(text.clone()),
_ => None,
})
.collect::<Vec<_>>()
.join("");
}
fn sync_node_items_from_children(node: &mut XmlNode) {
let mut child_index = 0usize;
let mut new_items = Vec::with_capacity(node.items.len().max(node.children.len()));
for item in &node.items {
match item {
XmlItem::Text(text) => new_items.push(XmlItem::Text(text.clone())),
XmlItem::Node(_) => {
if let Some(updated) = node.children.get(child_index) {
new_items.push(XmlItem::Node(updated.clone()));
child_index += 1;
}
}
}
}
while let Some(updated) = node.children.get(child_index) {
new_items.push(XmlItem::Node(updated.clone()));
child_index += 1;
}
node.items = new_items;
sync_node_children(node);
}
fn child_mut<'a>(node: &'a mut XmlNode, name: &str) -> Option<&'a mut XmlNode> {
node.children
.iter_mut()
.find(|child| child.local_name() == name || child.name == name)
}
#[allow(dead_code)]
fn _assert_send_sync() {
fn assert<T: Send + Sync>() {}
assert::<Result<Vec<u8>>>();
let _ = LoError::Parse(String::new());
}