use crate::{
ChartData, DocModel, DocumentComment, DocumentRevision, ExtractDocumentBlocksOptions,
OoxmlPackage, StructuredDocument,
};
use base64::Engine;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::BTreeMap;
#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub struct ExtractDocumentBundleOptions {
#[serde(flatten)]
pub blocks: ExtractDocumentBlocksOptions,
pub include_revisions: bool,
pub include_charts: bool,
pub deduplicate_assets: bool,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DocumentBundle {
#[serde(flatten)]
pub document: StructuredDocument,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub revisions: Option<Vec<DocumentRevision>>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub comments: Vec<DocumentComment>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub charts: BTreeMap<String, ChartData>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub assets: BTreeMap<String, String>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub revision_inlines: BTreeMap<String, Vec<crate::RevisionInline>>,
}
pub fn extract_document_bundle(
model: &DocModel,
package: &OoxmlPackage,
options: ExtractDocumentBundleOptions,
) -> DocumentBundle {
let (mut document, revisions, comments) = if options.include_revisions {
let reviewed = crate::extract_document_blocks_with_revisions(model, options.blocks);
(
reviewed.document,
Some(reviewed.revisions),
reviewed.comments,
)
} else {
(
crate::extract_document_blocks_with_options(model, options.blocks),
None,
crate::review::extract_document_comments(model),
)
};
let mut charts = BTreeMap::new();
if options.include_charts {
collect_chart_data(&document.blocks, package, &mut charts);
}
let revision_inlines = revisions
.as_ref()
.map(|r| crate::revision_inlines(&document.blocks, r))
.unwrap_or_default();
let assets = if options.deduplicate_assets {
extract_document_assets(&mut document.blocks)
} else {
BTreeMap::new()
};
DocumentBundle {
document,
revisions,
comments,
charts,
assets,
revision_inlines,
}
}
pub fn parse_document_bundle(
bytes: &[u8],
options: ExtractDocumentBundleOptions,
) -> Result<DocumentBundle, String> {
let mut package = crate::parse_document_bytes(bytes)?;
let model = crate::build_doc_model_with_options(
&package,
&crate::ParseOptions {
include_3d_models: options.blocks.include_3d_models,
..Default::default()
},
);
if !options.include_charts {
package.parts.clear();
package.binary_assets.clear();
}
Ok(extract_document_bundle(&model, &package, options))
}
pub fn extract_document_assets(blocks: &mut [crate::DocumentBlock]) -> BTreeMap<String, String> {
let mut assets = std::collections::HashMap::new();
collect_image_assets(blocks, &mut assets);
assets.into_iter().map(|(url, id)| (id, url)).collect()
}
pub fn collect_chart_data(
blocks: &[crate::DocumentBlock],
package: &crate::OoxmlPackage,
charts: &mut std::collections::BTreeMap<String, crate::ChartData>,
) {
for block in blocks {
if let crate::BlockDetails::Raw(details) = &block.details {
if let Some(part) = details.get("chartPartName").and_then(Value::as_str) {
if !charts.contains_key(part) {
if let Some(chart) = crate::extract_chart_data(package, part) {
charts.insert(part.to_owned(), chart);
}
}
}
}
collect_chart_data(&block.children, package, charts);
}
}
fn collect_image_assets(
blocks: &mut [crate::DocumentBlock],
assets: &mut std::collections::HashMap<String, String>,
) {
for block in blocks {
if let crate::BlockDetails::Raw(details) = &mut block.details {
if let Some(details) = details.as_object_mut() {
if let Some(model) = details.get_mut("model3d") {
if let Some(asset) = model.get_mut("asset").and_then(Value::as_object_mut) {
if let Some(Value::Array(data)) = asset.get("data") {
let bytes: Option<Vec<u8>> = data
.iter()
.map(|value| value.as_u64().and_then(|n| u8::try_from(n).ok()))
.collect();
if let Some(bytes) = bytes {
asset.remove("data");
let url = format!(
"data:model/gltf-binary;base64,{}",
base64::engine::general_purpose::STANDARD.encode(bytes)
);
let next = assets.len().to_string();
let id = assets.entry(url).or_insert(next).clone();
asset.insert("dataAssetId".into(), Value::String(id));
}
}
}
}
for field in ["image", "sourceImage", "previewImage"] {
let key = format!("{field}Url");
if let Some(url) = details.get(&key).and_then(Value::as_str).map(str::to_owned)
{
details.remove(&key);
let next = assets.len().to_string();
let id = assets.entry(url).or_insert(next).clone();
details.insert(format!("{field}AssetId"), Value::String(id));
}
}
}
}
collect_image_assets(&mut block.children, assets);
}
}