Skip to main content

excelize_rs/
doc_props.rs

1//! Document properties API (`docProps.go`).
2//!
3//! Provides read/write access to the application, core and custom document
4//! properties stored in `docProps/app.xml`, `docProps/core.xml` and
5//! `docProps/custom.xml`.
6
7use std::collections::HashMap;
8
9use chrono::{DateTime, NaiveDateTime};
10use quick_xml::de::from_reader as xml_from_reader;
11use quick_xml::se::to_string as xml_to_string;
12
13use crate::constants::{
14    CONTENT_TYPE_CUSTOM_PROPERTIES, DEFAULT_XML_PATH_DOC_PROPS_APP,
15    DEFAULT_XML_PATH_DOC_PROPS_CORE, DEFAULT_XML_PATH_DOC_PROPS_CUSTOM, DEFAULT_XML_PATH_RELS,
16    EXT_URI_CUSTOM_PROPERTY_FMT_ID, NAMESPACE_DOCUMENT_PROPERTIES_VARIANT_TYPES,
17    NAMESPACE_DUBLIN_CORE, NAMESPACE_DUBLIN_CORE_METADATA_INITIATIVE, NAMESPACE_DUBLIN_CORE_TERMS,
18    NAMESPACE_XML_SCHEMA_INSTANCE, SOURCE_RELATIONSHIP_CUSTOM_PROPERTIES,
19};
20use crate::errors::{ErrParameterInvalid, Result};
21use crate::file::File;
22use crate::xml::app::{AppProperties, XlsxProperties};
23use crate::xml::content_types::{XlsxContentTypeEntry, XlsxOverride};
24use crate::xml::core::{DecodeCoreProperties, DocProperties, XlsxCoreProperties, XlsxDcTerms};
25use crate::xml::custom::{
26    CustomProperty, CustomPropertyValue, DecodeCustomProperties, DecodeProperty,
27    XlsxCustomProperties, XlsxProperty,
28};
29
30const NAMESPACE_CUSTOM_PROPERTIES: &str =
31    "http://schemas.openxmlformats.org/officeDocument/2006/custom-properties";
32
33impl File {
34    // ------------------------------------------------------------------
35    // Public API
36    // ------------------------------------------------------------------
37
38    /// Set the document application properties (`docProps/app.xml`).
39    pub fn set_app_props(&mut self, props: &AppProperties) -> Result<()> {
40        let mut app = self.doc_props_app_reader()?;
41        app.application = non_empty(props.application.clone());
42        app.scale_crop = Some(props.scale_crop);
43        app.doc_security = Some(props.doc_security as i64);
44        app.company = non_empty(props.company.clone());
45        app.links_up_to_date = Some(props.links_up_to_date);
46        app.hyperlinks_changed = Some(props.hyperlinks_changed);
47        app.app_version = non_empty(props.app_version.clone());
48        self.doc_props_app_writer(&app);
49        Ok(())
50    }
51
52    /// Get the document application properties.
53    pub fn get_app_props(&self) -> Result<AppProperties> {
54        let app = self.doc_props_app_reader()?;
55        Ok(AppProperties {
56            application: app.application.unwrap_or_default(),
57            scale_crop: app.scale_crop.unwrap_or(false),
58            doc_security: app.doc_security.unwrap_or(0) as i32,
59            company: app.company.unwrap_or_default(),
60            links_up_to_date: app.links_up_to_date.unwrap_or(false),
61            hyperlinks_changed: app.hyperlinks_changed.unwrap_or(false),
62            app_version: app.app_version.unwrap_or_default(),
63        })
64    }
65
66    /// Set the document core properties (`docProps/core.xml`).
67    pub fn set_doc_props(&mut self, props: &DocProperties) -> Result<()> {
68        let core = self.doc_props_core_reader()?;
69        let mut new_props = XlsxCoreProperties {
70            dc: Some(NAMESPACE_DUBLIN_CORE.to_string()),
71            dcterms: Some(NAMESPACE_DUBLIN_CORE_TERMS.to_string()),
72            dcmitype: Some(NAMESPACE_DUBLIN_CORE_METADATA_INITIATIVE.to_string()),
73            xsi: Some(NAMESPACE_XML_SCHEMA_INSTANCE.to_string()),
74            title: core.title.clone(),
75            subject: core.subject.clone(),
76            creator: core.creator.clone(),
77            keywords: core.keywords.clone(),
78            description: core.description.clone(),
79            last_modified_by: core.last_modified_by.clone(),
80            language: core.language.clone(),
81            identifier: core.identifier.clone(),
82            revision: core.revision.clone(),
83            created: core.created.as_ref().map(|d| XlsxDcTerms {
84                text: d.text.clone(),
85                r#type: d.r#type.clone(),
86            }),
87            modified: core.modified.as_ref().map(|d| XlsxDcTerms {
88                text: d.text.clone(),
89                r#type: d.r#type.clone(),
90            }),
91            content_status: core.content_status.clone(),
92            category: core.category.clone(),
93            version: core.version.clone(),
94        };
95
96        if !props.category.is_empty() {
97            new_props.category = Some(props.category.clone());
98        } else {
99            new_props.category = None;
100        }
101        if !props.content_status.is_empty() {
102            new_props.content_status = Some(props.content_status.clone());
103        } else {
104            new_props.content_status = None;
105        }
106        if !props.creator.is_empty() {
107            new_props.creator = Some(props.creator.clone());
108        } else {
109            new_props.creator = None;
110        }
111        if !props.description.is_empty() {
112            new_props.description = Some(props.description.clone());
113        } else {
114            new_props.description = None;
115        }
116        if !props.identifier.is_empty() {
117            new_props.identifier = Some(props.identifier.clone());
118        } else {
119            new_props.identifier = None;
120        }
121        if !props.keywords.is_empty() {
122            new_props.keywords = Some(props.keywords.clone());
123        } else {
124            new_props.keywords = None;
125        }
126        if !props.last_modified_by.is_empty() {
127            new_props.last_modified_by = Some(props.last_modified_by.clone());
128        } else {
129            new_props.last_modified_by = None;
130        }
131        if !props.revision.is_empty() {
132            new_props.revision = Some(props.revision.clone());
133        } else {
134            new_props.revision = None;
135        }
136        if !props.subject.is_empty() {
137            new_props.subject = Some(props.subject.clone());
138        } else {
139            new_props.subject = None;
140        }
141        if !props.title.is_empty() {
142            new_props.title = Some(props.title.clone());
143        } else {
144            new_props.title = None;
145        }
146        if !props.language.is_empty() {
147            new_props.language = Some(props.language.clone());
148        } else {
149            new_props.language = None;
150        }
151        if !props.version.is_empty() {
152            new_props.version = Some(props.version.clone());
153        } else {
154            new_props.version = None;
155        }
156
157        if !props.created.is_empty() {
158            new_props.created = Some(XlsxDcTerms {
159                text: props.created.clone(),
160                r#type: Some("dcterms:W3CDTF".to_string()),
161            });
162        }
163        if !props.modified.is_empty() {
164            new_props.modified = Some(XlsxDcTerms {
165                text: props.modified.clone(),
166                r#type: Some("dcterms:W3CDTF".to_string()),
167            });
168        }
169
170        self.doc_props_core_writer(&new_props);
171        Ok(())
172    }
173
174    /// Get the document core properties.
175    pub fn get_doc_props(&self) -> Result<DocProperties> {
176        let core = self.doc_props_core_reader()?;
177        let mut ret = DocProperties {
178            category: core.category.unwrap_or_default(),
179            content_status: core.content_status.unwrap_or_default(),
180            created: String::new(),
181            creator: core.creator.unwrap_or_default(),
182            description: core.description.unwrap_or_default(),
183            identifier: core.identifier.unwrap_or_default(),
184            keywords: core.keywords.unwrap_or_default(),
185            last_modified_by: core.last_modified_by.unwrap_or_default(),
186            modified: String::new(),
187            revision: core.revision.unwrap_or_default(),
188            subject: core.subject.unwrap_or_default(),
189            title: core.title.unwrap_or_default(),
190            language: core.language.unwrap_or_default(),
191            version: core.version.unwrap_or_default(),
192        };
193        if let Some(c) = core.created {
194            ret.created = c.text;
195        }
196        if let Some(m) = core.modified {
197            ret.modified = m.text;
198        }
199        Ok(ret)
200    }
201
202    /// Set a custom document property by name and value. A property with a
203    /// `None` value is deleted.
204    pub fn set_custom_props(&mut self, prop: &CustomProperty) -> Result<()> {
205        if prop.name.is_empty() {
206            return Err(Box::new(ErrParameterInvalid));
207        }
208
209        let custom = self.custom_properties_reader()?;
210
211        let mut by_name: HashMap<String, XlsxProperty> = HashMap::new();
212        let mut max_pid: i64 = 1;
213        for p in &custom.property {
214            let name = p.name.clone().unwrap_or_default();
215            if !name.is_empty() {
216                max_pid = max_pid.max(p.pid);
217                by_name.insert(name, decode_property_to_xlsx(p));
218            }
219        }
220
221        match &prop.value {
222            None => {
223                by_name.remove(&prop.name);
224            }
225            Some(value) => {
226                let pid = if let Some(existing) = by_name.get(&prop.name) {
227                    existing.pid
228                } else {
229                    max_pid += 1;
230                    max_pid
231                };
232                by_name.insert(
233                    prop.name.clone(),
234                    XlsxProperty {
235                        fmt_id: EXT_URI_CUSTOM_PROPERTY_FMT_ID.to_string(),
236                        pid,
237                        name: Some(prop.name.clone()),
238                        link_target: None,
239                        ..custom_value_to_xlsx(value)?
240                    },
241                );
242            }
243        }
244
245        self.ensure_custom_properties_relationship();
246        self.add_custom_properties_content_type()?;
247
248        let mut properties: Vec<XlsxProperty> = by_name.into_values().collect();
249        properties.sort_by_key(|p| p.pid);
250        self.custom_properties_writer(&XlsxCustomProperties {
251            xmlns: Some(NAMESPACE_CUSTOM_PROPERTIES.to_string()),
252            vt: Some(NAMESPACE_DOCUMENT_PROPERTIES_VARIANT_TYPES.to_string()),
253            property: properties,
254        });
255        Ok(())
256    }
257
258    /// Get all custom document properties.
259    pub fn get_custom_props(&self) -> Result<Vec<CustomProperty>> {
260        let custom = self.custom_properties_reader()?;
261        let mut props = Vec::new();
262        for p in &custom.property {
263            props.push(CustomProperty {
264                name: p.name.clone().unwrap_or_default(),
265                value: decode_property_value(p),
266            });
267        }
268        Ok(props)
269    }
270
271    /// Delete a custom document property by name.
272    pub fn delete_custom_props(&mut self, name: &str) -> Result<()> {
273        if name.is_empty() {
274            return Err(Box::new(ErrParameterInvalid));
275        }
276        self.set_custom_props(&CustomProperty {
277            name: name.to_string(),
278            value: None,
279        })
280    }
281
282    // ------------------------------------------------------------------
283    // Internal readers / writers
284    // ------------------------------------------------------------------
285
286    fn doc_props_app_reader(&self) -> Result<XlsxProperties> {
287        let data = crate::file::namespace_strict_to_transitional(
288            &self.read_xml(DEFAULT_XML_PATH_DOC_PROPS_APP),
289        );
290        if data.is_empty() {
291            return Ok(XlsxProperties::default());
292        }
293        if !self.xml_attr.contains_key(DEFAULT_XML_PATH_DOC_PROPS_APP) {
294            if let Some(attrs) = crate::file::extract_root_namespace_attributes(&data) {
295                self.xml_attr
296                    .insert(DEFAULT_XML_PATH_DOC_PROPS_APP.to_string(), attrs);
297            }
298        }
299        Ok(xml_from_reader(data.as_slice())?)
300    }
301
302    fn doc_props_core_reader(&self) -> Result<DecodeCoreProperties> {
303        let data = crate::file::namespace_strict_to_transitional(
304            &self.read_xml(DEFAULT_XML_PATH_DOC_PROPS_CORE),
305        );
306        if data.is_empty() {
307            return Ok(DecodeCoreProperties::default());
308        }
309        if !self.xml_attr.contains_key(DEFAULT_XML_PATH_DOC_PROPS_CORE) {
310            if let Some(attrs) = crate::file::extract_root_namespace_attributes(&data) {
311                self.xml_attr
312                    .insert(DEFAULT_XML_PATH_DOC_PROPS_CORE.to_string(), attrs);
313            }
314        }
315        Ok(xml_from_reader(data.as_slice())?)
316    }
317
318    fn custom_properties_reader(&self) -> Result<DecodeCustomProperties> {
319        let data = crate::file::namespace_strict_to_transitional(
320            &self.read_xml(DEFAULT_XML_PATH_DOC_PROPS_CUSTOM),
321        );
322        if data.is_empty() {
323            return Ok(DecodeCustomProperties::default());
324        }
325        Ok(xml_from_reader(data.as_slice())?)
326    }
327
328    fn doc_props_app_writer(&self, props: &XlsxProperties) {
329        if let Ok(mut output) = xml_to_string(props).map(|s| s.into_bytes()) {
330            self.replace_namespace_bytes_if_needed(DEFAULT_XML_PATH_DOC_PROPS_APP, &mut output);
331            self.save_file_list(DEFAULT_XML_PATH_DOC_PROPS_APP, &output);
332        }
333    }
334
335    fn doc_props_core_writer(&self, props: &XlsxCoreProperties) {
336        if let Ok(mut output) = xml_to_string(props).map(|s| s.into_bytes()) {
337            self.replace_namespace_bytes_if_needed(DEFAULT_XML_PATH_DOC_PROPS_CORE, &mut output);
338            // The serialization struct uses the local name `coreProperties`;
339            // OOXML requires the `cp` prefix on this element.
340            if let Ok(s) = std::str::from_utf8(&output) {
341                let s = s
342                    .replacen("<coreProperties", "<cp:coreProperties", 1)
343                    .replacen("</coreProperties>", "</cp:coreProperties>", 1);
344                output = s.into_bytes();
345            }
346            self.save_file_list(DEFAULT_XML_PATH_DOC_PROPS_CORE, &output);
347        }
348    }
349
350    fn custom_properties_writer(&self, props: &XlsxCustomProperties) {
351        if let Ok(output) = xml_to_string(props).map(|s| s.into_bytes()) {
352            self.save_file_list(DEFAULT_XML_PATH_DOC_PROPS_CUSTOM, &output);
353        }
354    }
355
356    // ------------------------------------------------------------------
357    // Helpers
358    // ------------------------------------------------------------------
359
360    fn ensure_custom_properties_relationship(&self) {
361        if let Ok(Some(rels)) = self.rels_reader(DEFAULT_XML_PATH_RELS) {
362            if rels.relationships.iter().any(|r| {
363                r.r#type == SOURCE_RELATIONSHIP_CUSTOM_PROPERTIES
364                    && r.target == DEFAULT_XML_PATH_DOC_PROPS_CUSTOM
365            }) {
366                return;
367            }
368        }
369        self.add_rels(
370            DEFAULT_XML_PATH_RELS,
371            SOURCE_RELATIONSHIP_CUSTOM_PROPERTIES,
372            DEFAULT_XML_PATH_DOC_PROPS_CUSTOM,
373            "",
374        );
375    }
376
377    fn add_custom_properties_content_type(&self) -> Result<()> {
378        let mut ct = self.content_types_reader()?;
379        let exists = ct.entries.iter().any(|e| match e {
380            XlsxContentTypeEntry::Override(o) => {
381                o.part_name == "/docProps/custom.xml"
382                    && o.content_type == CONTENT_TYPE_CUSTOM_PROPERTIES
383            }
384            _ => false,
385        });
386        if !exists {
387            ct.entries
388                .push(XlsxContentTypeEntry::Override(XlsxOverride {
389                    part_name: "/docProps/custom.xml".to_string(),
390                    content_type: CONTENT_TYPE_CUSTOM_PROPERTIES.to_string(),
391                }));
392            *self.content_types.lock().unwrap() = Some(ct);
393        }
394        Ok(())
395    }
396}
397
398fn non_empty(s: String) -> Option<String> {
399    if s.is_empty() { None } else { Some(s) }
400}
401
402fn decode_property_to_xlsx(p: &DecodeProperty) -> XlsxProperty {
403    XlsxProperty {
404        fmt_id: p.fmt_id.clone(),
405        pid: p.pid,
406        name: p.name.clone(),
407        link_target: p.link_target.clone(),
408        vector: p.vector.clone(),
409        array: p.array.clone(),
410        blob: p.blob.clone(),
411        oblob: p.oblob.clone(),
412        empty: p.empty.clone(),
413        null: p.null.clone(),
414        i1: p.i1,
415        i2: p.i2,
416        i4: p.i4,
417        i8: p.i8,
418        int: p.int,
419        ui1: p.ui1,
420        ui2: p.ui2,
421        ui4: p.ui4,
422        ui8: p.ui8,
423        uint: p.uint,
424        r4: p.r4,
425        r8: p.r8,
426        decimal: p.decimal.clone(),
427        lpstr: p.lpstr.clone(),
428        lpwstr: p.lpwstr.clone(),
429        bstr: p.bstr.clone(),
430        date: p.date.clone(),
431        file_time: p.file_time.clone(),
432        r#bool: p.r#bool,
433        cy: p.cy.clone(),
434        error: p.error.clone(),
435        stream: p.stream.clone(),
436        ostream: p.ostream.clone(),
437        storage: p.storage.clone(),
438        ostorage: p.ostorage.clone(),
439        vstream: p.vstream.clone(),
440        cls_id: p.cls_id.clone(),
441    }
442}
443
444fn custom_value_to_xlsx(value: &CustomPropertyValue) -> Result<XlsxProperty> {
445    let mut p = XlsxProperty::default();
446    match value {
447        CustomPropertyValue::Int(v) => p.i4 = Some(*v),
448        CustomPropertyValue::Float(v) => p.r8 = Some(*v),
449        CustomPropertyValue::Bool(v) => p.r#bool = Some(*v),
450        CustomPropertyValue::String(v) => p.lpwstr = Some(v.clone()),
451        CustomPropertyValue::Date(v) => {
452            validate_date_string(v)?;
453            p.file_time = Some(v.clone());
454        }
455    }
456    Ok(p)
457}
458
459fn validate_date_string(s: &str) -> Result<()> {
460    if DateTime::parse_from_rfc3339(s).is_ok() {
461        return Ok(());
462    }
463    if NaiveDateTime::parse_from_str(s, "%Y-%m-%dT%H:%M:%S%.f").is_ok() {
464        return Ok(());
465    }
466    Err(Box::new(ErrParameterInvalid))
467}
468
469fn decode_property_value(p: &DecodeProperty) -> Option<CustomPropertyValue> {
470    if let Some(v) = p.i1 {
471        return Some(CustomPropertyValue::Int(v as i32));
472    }
473    if let Some(v) = p.i2 {
474        return Some(CustomPropertyValue::Int(v as i32));
475    }
476    if let Some(v) = p.i4 {
477        return Some(CustomPropertyValue::Int(v));
478    }
479    if let Some(v) = p.i8 {
480        return Some(CustomPropertyValue::Int(v as i32));
481    }
482    if let Some(v) = p.int {
483        return Some(CustomPropertyValue::Int(v as i32));
484    }
485    if let Some(v) = p.ui1 {
486        return Some(CustomPropertyValue::Int(v as i32));
487    }
488    if let Some(v) = p.ui2 {
489        return Some(CustomPropertyValue::Int(v as i32));
490    }
491    if let Some(v) = p.ui4 {
492        return Some(CustomPropertyValue::Int(v as i32));
493    }
494    if let Some(v) = p.ui8 {
495        return Some(CustomPropertyValue::Int(v as i32));
496    }
497    if let Some(v) = p.uint {
498        return Some(CustomPropertyValue::Int(v as i32));
499    }
500    if let Some(v) = p.r4 {
501        return Some(CustomPropertyValue::Float(v as f64));
502    }
503    if let Some(v) = p.r8 {
504        return Some(CustomPropertyValue::Float(v));
505    }
506    if let Some(v) = p.r#bool {
507        return Some(CustomPropertyValue::Bool(v));
508    }
509    if let Some(v) = &p.lpwstr {
510        return Some(CustomPropertyValue::String(v.clone()));
511    }
512    if let Some(v) = &p.file_time {
513        return Some(CustomPropertyValue::Date(v.clone()));
514    }
515    if let Some(v) = &p.lpstr {
516        return Some(CustomPropertyValue::String(v.clone()));
517    }
518    None
519}