1use crate::{
3 ChartData, DocModel, DocumentComment, DocumentRevision, ExtractDocumentBlocksOptions,
4 OoxmlPackage, StructuredDocument,
5};
6use base64::Engine;
7use serde::{Deserialize, Serialize};
8use serde_json::Value;
9use std::collections::BTreeMap;
10
11#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize)]
12#[serde(default, rename_all = "camelCase")]
13pub struct ExtractDocumentBundleOptions {
14 #[serde(flatten)]
15 pub blocks: ExtractDocumentBlocksOptions,
16 pub include_revisions: bool,
17 pub include_charts: bool,
18 pub deduplicate_assets: bool,
19}
20
21#[derive(Clone, Debug, Serialize, Deserialize)]
22#[serde(rename_all = "camelCase")]
23pub struct DocumentBundle {
24 #[serde(flatten)]
25 pub document: StructuredDocument,
26 #[serde(default, skip_serializing_if = "Option::is_none")]
27 pub revisions: Option<Vec<DocumentRevision>>,
28 #[serde(default, skip_serializing_if = "Vec::is_empty")]
29 pub comments: Vec<DocumentComment>,
30 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
31 pub charts: BTreeMap<String, ChartData>,
32 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
34 pub assets: BTreeMap<String, String>,
35 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
36 pub revision_inlines: BTreeMap<String, Vec<crate::RevisionInline>>,
37}
38
39pub fn extract_document_bundle(
40 model: &DocModel,
41 package: &OoxmlPackage,
42 options: ExtractDocumentBundleOptions,
43) -> DocumentBundle {
44 let (mut document, revisions, comments) = if options.include_revisions {
45 let reviewed = crate::extract_document_blocks_with_revisions(model, options.blocks);
46 (
47 reviewed.document,
48 Some(reviewed.revisions),
49 reviewed.comments,
50 )
51 } else {
52 (
53 crate::extract_document_blocks_with_options(model, options.blocks),
54 None,
55 crate::review::extract_document_comments(model),
56 )
57 };
58 let mut charts = BTreeMap::new();
59 if options.include_charts {
60 collect_chart_data(&document.blocks, package, &mut charts);
61 }
62 let revision_inlines = revisions
63 .as_ref()
64 .map(|r| crate::revision_inlines(&document.blocks, r))
65 .unwrap_or_default();
66 let assets = if options.deduplicate_assets {
67 extract_document_assets(&mut document.blocks)
68 } else {
69 BTreeMap::new()
70 };
71 DocumentBundle {
72 document,
73 revisions,
74 comments,
75 charts,
76 assets,
77 revision_inlines,
78 }
79}
80
81pub fn parse_document_bundle(
82 bytes: &[u8],
83 options: ExtractDocumentBundleOptions,
84) -> Result<DocumentBundle, String> {
85 let mut package = crate::parse_document_bytes(bytes)?;
86 let model = crate::build_doc_model_with_options(
87 &package,
88 &crate::ParseOptions {
89 include_3d_models: options.blocks.include_3d_models,
90 ..Default::default()
91 },
92 );
93 if !options.include_charts {
94 package.parts.clear();
95 package.binary_assets.clear();
96 }
97 Ok(extract_document_bundle(&model, &package, options))
98}
99
100pub fn extract_document_assets(blocks: &mut [crate::DocumentBlock]) -> BTreeMap<String, String> {
102 let mut assets = std::collections::HashMap::new();
103 collect_image_assets(blocks, &mut assets);
104 assets.into_iter().map(|(url, id)| (id, url)).collect()
105}
106
107pub fn collect_chart_data(
108 blocks: &[crate::DocumentBlock],
109 package: &crate::OoxmlPackage,
110 charts: &mut std::collections::BTreeMap<String, crate::ChartData>,
111) {
112 for block in blocks {
113 if let crate::BlockDetails::Raw(details) = &block.details {
114 if let Some(part) = details.get("chartPartName").and_then(Value::as_str) {
115 if !charts.contains_key(part) {
116 if let Some(chart) = crate::extract_chart_data(package, part) {
117 charts.insert(part.to_owned(), chart);
118 }
119 }
120 }
121 }
122 collect_chart_data(&block.children, package, charts);
123 }
124}
125
126fn collect_image_assets(
127 blocks: &mut [crate::DocumentBlock],
128 assets: &mut std::collections::HashMap<String, String>,
129) {
130 for block in blocks {
131 if let crate::BlockDetails::Raw(details) = &mut block.details {
132 if let Some(details) = details.as_object_mut() {
133 if let Some(model) = details.get_mut("model3d") {
134 if let Some(asset) = model.get_mut("asset").and_then(Value::as_object_mut) {
135 if let Some(Value::Array(data)) = asset.get("data") {
136 let bytes: Option<Vec<u8>> = data
137 .iter()
138 .map(|value| value.as_u64().and_then(|n| u8::try_from(n).ok()))
139 .collect();
140 if let Some(bytes) = bytes {
141 asset.remove("data");
142 let url = format!(
143 "data:model/gltf-binary;base64,{}",
144 base64::engine::general_purpose::STANDARD.encode(bytes)
145 );
146 let next = assets.len().to_string();
147 let id = assets.entry(url).or_insert(next).clone();
148 asset.insert("dataAssetId".into(), Value::String(id));
149 }
150 }
151 }
152 }
153 for field in ["image", "sourceImage", "previewImage"] {
154 let key = format!("{field}Url");
155 if let Some(url) = details.get(&key).and_then(Value::as_str).map(str::to_owned)
156 {
157 details.remove(&key);
158 let next = assets.len().to_string();
159 let id = assets.entry(url).or_insert(next).clone();
160 details.insert(format!("{field}AssetId"), Value::String(id));
161 }
162 }
163 }
164 }
165 collect_image_assets(&mut block.children, assets);
166 }
167}