1use crate::cell_::CellData;
2use crate::config::{ConfigItem, ConfigItemType, ConfigValue};
3use crate::draw::{Annotation, DrawFrame, DrawFrameContent, DrawImage};
4use crate::error::OdsError;
5use crate::format::{FormatPartType, ValueFormatTrait};
6use crate::io::format::{format_duration2, format_validation_condition};
7use crate::io::xmlwriter::XmlWriter;
8use crate::io::NamespaceMap;
9use crate::manifest::Manifest;
10use crate::metadata::MetaValue;
11use crate::refs::{format_cellranges, CellRange};
12use crate::sheet::Visibility;
13use crate::sheet_::{dedup_colheader, CellDataIter};
14use crate::style::{
15 CellStyle, ColStyle, FontFaceDecl, GraphicStyle, HeaderFooter, MasterPage, MasterPageRef,
16 PageStyle, PageStyleRef, ParagraphStyle, RowStyle, RubyStyle, StyleOrigin, StyleUse,
17 TableStyle, TextStyle,
18};
19use crate::validation::ValidationDisplay;
20use crate::workbook::{EventListener, Script};
21use crate::xmltree::{XmlContent, XmlTag};
22use crate::HashMap;
23use crate::{Length, Sheet, Value, ValueType, WorkBook};
24use std::borrow::Cow;
25use std::cmp::max;
26use std::collections::{BTreeMap, HashSet};
27use std::fs::File;
28use std::io::{BufWriter, Cursor, Seek, Write};
29use std::path::Path;
30use std::{io, mem};
31use zip::write::FileOptions;
32use zip::{CompressionMethod, ZipWriter};
33
34#[cfg(test)]
35mod tests;
36
37type OdsXmlWriter<'a> = XmlWriter<&'a mut dyn Write>;
38
39const DATETIME_FORMAT: &str = "%Y-%m-%dT%H:%M:%S%.f";
40
41#[allow(dead_code)]
42trait SeekWrite: Seek + Write {}
43
44impl<T> SeekWrite for T where T: Seek + Write {}
45
46#[derive(Debug, Default)]
48pub struct OdsWriteOptions {
49 method: CompressionMethod,
50 level: Option<i64>,
51}
52
53impl OdsWriteOptions {
54 pub fn compression_method(mut self, method: CompressionMethod) -> Self {
56 self.method = method;
57 self
58 }
59
60 pub fn compression_level(mut self, level: Option<i64>) -> Self {
62 self.level = level;
63 self
64 }
65
66 pub fn write_ods<T: Write + Seek>(
68 self,
69 book: &mut WorkBook,
70 mut write: T,
71 ) -> Result<(), OdsError> {
72 let w = ZipWriter::new(&mut write);
73
74 write_ods_impl(self, w, book)?;
75
76 Ok(())
77 }
78}
79
80pub fn write_ods_buf_uncompressed(book: &mut WorkBook, buf: Vec<u8>) -> Result<Vec<u8>, OdsError> {
82 let mut cursor = Cursor::new(buf);
83
84 OdsWriteOptions::default()
85 .compression_method(CompressionMethod::Stored)
86 .write_ods(book, &mut cursor)?;
87
88 Ok(cursor.into_inner())
89}
90
91pub fn write_ods_buf(book: &mut WorkBook, buf: Vec<u8>) -> Result<Vec<u8>, OdsError> {
93 let mut cursor = Cursor::new(buf);
94
95 OdsWriteOptions::default()
96 .compression_method(CompressionMethod::Deflated)
97 .write_ods(book, &mut cursor)?;
98
99 Ok(cursor.into_inner())
100}
101
102pub fn write_ods_to<T: Write + Seek>(book: &mut WorkBook, mut write: T) -> Result<(), OdsError> {
104 OdsWriteOptions::default()
105 .compression_method(CompressionMethod::Deflated)
106 .write_ods(book, &mut write)?;
107
108 Ok(())
109}
110
111pub fn write_ods<P: AsRef<Path>>(book: &mut WorkBook, ods_path: P) -> Result<(), OdsError> {
113 let mut write = BufWriter::new(File::create(ods_path)?);
114
115 OdsWriteOptions::default()
116 .compression_method(CompressionMethod::Deflated)
117 .write_ods(book, &mut write)?;
118
119 write.flush()?;
120
121 Ok(())
122}
123
124pub fn write_fods_buf(book: &mut WorkBook, mut buf: Vec<u8>) -> Result<Vec<u8>, OdsError> {
126 let write: &mut dyn Write = &mut buf;
127
128 write_fods_impl(write, book)?;
129
130 Ok(buf)
131}
132
133pub fn write_fods_to<T: Write + Seek>(book: &mut WorkBook, mut write: T) -> Result<(), OdsError> {
135 let write: &mut dyn Write = &mut write;
136
137 write_fods_impl(write, book)?;
138
139 Ok(())
140}
141
142pub fn write_fods<P: AsRef<Path>>(book: &mut WorkBook, fods_path: P) -> Result<(), OdsError> {
144 let mut write = BufWriter::new(File::create(fods_path)?);
145 let write: &mut dyn Write = &mut write;
146
147 write_fods_impl(write, book)?;
148
149 Ok(())
150}
151
152fn write_fods_impl(writer: &mut dyn Write, book: &mut WorkBook) -> Result<(), OdsError> {
155 sanity_checks(book)?;
156 calculations(book)?;
157
158 convert(book)?;
159
160 let mut xml_out = XmlWriter::new(writer).line_break(true);
161 write_fods_content(book, &mut xml_out)?;
162
163 Ok(())
164}
165
166fn convert(book: &mut WorkBook) -> Result<(), OdsError> {
167 for v in book.tablestyles.values_mut() {
168 v.set_origin(StyleOrigin::Content);
169 }
170 for v in book.rowstyles.values_mut() {
171 v.set_origin(StyleOrigin::Content);
172 }
173 for v in book.colstyles.values_mut() {
174 v.set_origin(StyleOrigin::Content);
175 }
176 for v in book.cellstyles.values_mut() {
177 v.set_origin(StyleOrigin::Content);
178 }
179 for v in book.paragraphstyles.values_mut() {
180 v.set_origin(StyleOrigin::Content);
181 }
182 for v in book.textstyles.values_mut() {
183 v.set_origin(StyleOrigin::Content);
184 }
185 for v in book.rubystyles.values_mut() {
186 v.set_origin(StyleOrigin::Content);
187 }
188 for v in book.graphicstyles.values_mut() {
189 v.set_origin(StyleOrigin::Content);
190 }
191
192 for v in book.formats_boolean.values_mut() {
193 v.set_origin(StyleOrigin::Content);
194 }
195 for v in book.formats_number.values_mut() {
196 v.set_origin(StyleOrigin::Content);
197 }
198 for v in book.formats_percentage.values_mut() {
199 v.set_origin(StyleOrigin::Content);
200 }
201 for v in book.formats_currency.values_mut() {
202 v.set_origin(StyleOrigin::Content);
203 }
204 for v in book.formats_text.values_mut() {
205 v.set_origin(StyleOrigin::Content);
206 }
207 for v in book.formats_datetime.values_mut() {
208 v.set_origin(StyleOrigin::Content);
209 }
210 for v in book.formats_timeduration.values_mut() {
211 v.set_origin(StyleOrigin::Content);
212 }
213
214 Ok(())
215}
216
217fn write_fods_content(book: &mut WorkBook, xml_out: &mut OdsXmlWriter<'_>) -> Result<(), OdsError> {
218 let xmlns = book
219 .xmlns
220 .entry("meta.xml".into())
221 .or_insert_with(NamespaceMap::new);
222
223 xmlns.insert_str(
224 "xmlns:office",
225 "urn:oasis:names:tc:opendocument:xmlns:office:1.0",
226 );
227 xmlns.insert_str("xmlns:ooo", "http://openoffice.org/2004/office");
228 xmlns.insert_str(
229 "xmlns:fo",
230 "urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0",
231 );
232 xmlns.insert_str("xmlns:xlink", "http://www.w3.org/1999/xlink");
233 xmlns.insert_str(
234 "xmlns:config",
235 "urn:oasis:names:tc:opendocument:xmlns:config:1.0",
236 );
237 xmlns.insert_str("xmlns:dc", "http://purl.org/dc/elements/1.1/");
238 xmlns.insert_str(
239 "xmlns:meta",
240 "urn:oasis:names:tc:opendocument:xmlns:meta:1.0",
241 );
242 xmlns.insert_str(
243 "xmlns:style",
244 "urn:oasis:names:tc:opendocument:xmlns:style:1.0",
245 );
246 xmlns.insert_str(
247 "xmlns:text",
248 "urn:oasis:names:tc:opendocument:xmlns:text:1.0",
249 );
250 xmlns.insert_str("xmlns:rpt", "http://openoffice.org/2005/report");
251 xmlns.insert_str(
252 "xmlns:draw",
253 "urn:oasis:names:tc:opendocument:xmlns:drawing:1.0",
254 );
255 xmlns.insert_str(
256 "xmlns:dr3d",
257 "urn:oasis:names:tc:opendocument:xmlns:dr3d:1.0",
258 );
259 xmlns.insert_str(
260 "xmlns:svg",
261 "urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0",
262 );
263 xmlns.insert_str(
264 "xmlns:chart",
265 "urn:oasis:names:tc:opendocument:xmlns:chart:1.0",
266 );
267 xmlns.insert_str(
268 "xmlns:table",
269 "urn:oasis:names:tc:opendocument:xmlns:table:1.0",
270 );
271 xmlns.insert_str(
272 "xmlns:number",
273 "urn:oasis:names:tc:opendocument:xmlns:datastyle:1.0",
274 );
275 xmlns.insert_str("xmlns:ooow", "http://openoffice.org/2004/writer");
276 xmlns.insert_str("xmlns:oooc", "http://openoffice.org/2004/calc");
277 xmlns.insert_str("xmlns:of", "urn:oasis:names:tc:opendocument:xmlns:of:1.2");
278 xmlns.insert_str("xmlns:xforms", "http://www.w3.org/2002/xforms");
279 xmlns.insert_str("xmlns:tableooo", "http://openoffice.org/2009/table");
280 xmlns.insert_str(
281 "xmlns:calcext",
282 "urn:org:documentfoundation:names:experimental:calc:xmlns:calcext:1.0",
283 );
284 xmlns.insert_str("xmlns:drawooo", "http://openoffice.org/2010/draw");
285 xmlns.insert_str(
286 "xmlns:loext",
287 "urn:org:documentfoundation:names:experimental:office:xmlns:loext:1.0",
288 );
289 xmlns.insert_str(
290 "xmlns:field",
291 "urn:openoffice:names:experimental:ooo-ms-interop:xmlns:field:1.0",
292 );
293 xmlns.insert_str("xmlns:math", "http://www.w3.org/1998/Math/MathML");
294 xmlns.insert_str(
295 "xmlns:form",
296 "urn:oasis:names:tc:opendocument:xmlns:form:1.0",
297 );
298 xmlns.insert_str(
299 "xmlns:script",
300 "urn:oasis:names:tc:opendocument:xmlns:script:1.0",
301 );
302 xmlns.insert_str(
303 "xmlns:formx",
304 "urn:openoffice:names:experimental:ooxml-odf-interop:xmlns:form:1.0",
305 );
306 xmlns.insert_str("xmlns:dom", "http://www.w3.org/2001/xml-events");
307 xmlns.insert_str("xmlns:xsd", "http://www.w3.org/2001/XMLSchema");
308 xmlns.insert_str("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance");
309 xmlns.insert_str("xmlns:xhtml", "http://www.w3.org/1999/xhtml");
310 xmlns.insert_str("xmlns:grddl", "http://www.w3.org/2003/g/data-view#");
311 xmlns.insert_str("xmlns:css3t", "http://www.w3.org/TR/css3-text/");
312 xmlns.insert_str(
313 "xmlns:presentation",
314 "urn:oasis:names:tc:opendocument:xmlns:presentation:1.0",
315 );
316
317 xml_out.dtd("UTF-8")?;
318
319 xml_out.elem("office:document")?;
320 write_xmlns(xmlns, xml_out)?;
321 xml_out.attr_esc("office:version", book.version())?;
322 xml_out.attr_esc(
323 "office:mimetype",
324 "application/vnd.oasis.opendocument.spreadsheet",
325 )?;
326
327 write_office_meta(book, xml_out)?;
328 write_office_settings(book, xml_out)?;
329 write_office_scripts(book, xml_out)?;
330 write_office_font_face_decls(book, StyleOrigin::Content, xml_out)?;
331 write_office_styles(book, StyleOrigin::Content, xml_out)?;
332 write_office_automatic_styles(book, StyleOrigin::Content, xml_out)?;
333 write_office_master_styles(book, xml_out)?;
334 write_office_body(book, xml_out)?;
335
336 xml_out.end_elem("office:document")?;
337
338 xml_out.close()?;
339
340 Ok(())
341}
342
343fn write_ods_impl<W: Write + Seek>(
346 cfg: OdsWriteOptions,
347 mut zip_writer: ZipWriter<W>,
348 book: &mut WorkBook,
349) -> Result<(), OdsError> {
350 sanity_checks(book)?;
351 calculations(book)?;
352
353 create_manifest(book)?;
354
355 zip_writer.start_file(
356 "mimetype",
357 FileOptions::<()>::default().compression_method(CompressionMethod::Stored),
358 )?;
359 write_ods_mimetype(&mut zip_writer)?;
360
361 zip_writer.add_directory("META-INF", FileOptions::<()>::default())?;
362 zip_writer.start_file(
363 "META-INF/manifest.xml",
364 FileOptions::<()>::default()
365 .compression_method(cfg.method)
366 .compression_level(cfg.level),
367 )?;
368 write_ods_manifest(book, &mut XmlWriter::new(&mut zip_writer))?;
369
370 zip_writer.start_file(
371 "meta.xml",
372 FileOptions::<()>::default()
373 .compression_method(cfg.method)
374 .compression_level(cfg.level),
375 )?;
376 write_ods_metadata(book, &mut XmlWriter::new(&mut zip_writer))?;
377
378 zip_writer.start_file(
379 "settings.xml",
380 FileOptions::<()>::default()
381 .compression_method(cfg.method)
382 .compression_level(cfg.level),
383 )?;
384 write_ods_settings(book, &mut XmlWriter::new(&mut zip_writer))?;
385
386 zip_writer.start_file(
387 "styles.xml",
388 FileOptions::<()>::default()
389 .compression_method(cfg.method)
390 .compression_level(cfg.level),
391 )?;
392 write_ods_styles(book, &mut XmlWriter::new(&mut zip_writer))?;
393
394 zip_writer.start_file(
395 "content.xml",
396 FileOptions::<()>::default()
397 .compression_method(cfg.method)
398 .compression_level(cfg.level),
399 )?;
400 write_ods_content(book, &mut XmlWriter::new(&mut zip_writer))?;
401
402 write_ods_extra(&cfg, &mut zip_writer, book)?;
403
404 zip_writer.finish()?;
405
406 Ok(())
407}
408
409fn sanity_checks(book: &mut WorkBook) -> Result<(), OdsError> {
411 if book.sheets.is_empty() {
412 return Err(OdsError::Ods("Workbook contains no sheets.".to_string()));
413 }
414 Ok(())
415}
416
417fn calculations(book: &mut WorkBook) -> Result<(), OdsError> {
419 calc_metadata(book)?;
420 calc_config(book)?;
421
422 calc_row_header_styles(book)?;
423 calc_col_header_styles(book)?;
424 calc_col_headers(book)?;
425
426 Ok(())
427}
428
429fn calc_col_headers(book: &mut WorkBook) -> Result<(), OdsError> {
431 for i in 0..book.num_sheets() {
432 let mut sheet = book.detach_sheet(i);
433
434 dedup_colheader(&mut sheet)?;
436
437 let mut split_pos = HashSet::new();
439 for grp in &sheet.group_cols {
440 split_pos.insert(grp.from);
441 split_pos.insert(grp.to + 1);
442 }
443 if let Some(header_cols) = &sheet.header_cols {
444 split_pos.insert(header_cols.from);
445 split_pos.insert(header_cols.to + 1);
446 }
447
448 let col_header = mem::take(&mut sheet.col_header);
449 let mut new_col_header = BTreeMap::new();
450
451 for (mut col, mut header) in col_header {
452 let mut cc = col;
453 loop {
454 if cc == col + header.span {
455 new_col_header.insert(col, header);
456 break;
457 }
458
459 if split_pos.contains(&cc) {
460 let new_span = cc - col;
461 if new_span > 0 {
462 let mut new_header = header.clone();
463 new_header.span = new_span;
464 new_col_header.insert(col, new_header);
465 }
466
467 header.span -= new_span;
468 col = cc;
469 }
470
471 cc += 1;
472 }
473 }
474
475 sheet.col_header = new_col_header;
476
477 book.attach_sheet(sheet);
478 }
479
480 Ok(())
481}
482
483fn calc_col_header_styles(book: &mut WorkBook) -> Result<(), OdsError> {
485 for i in 0..book.num_sheets() {
486 let mut sheet = book.detach_sheet(i);
487
488 for ch in sheet.col_header.values_mut() {
490 if ch.width != Length::Default && ch.style.is_none() {
492 let colstyle = book.add_colstyle(ColStyle::new_empty());
493 ch.style = Some(colstyle);
494 }
495
496 if let Some(style_name) = ch.style.as_ref() {
498 if let Some(style) = book.colstyle_mut(style_name) {
499 if ch.width == Length::Default {
500 style.set_use_optimal_col_width(true);
501 style.set_col_width(Length::Default);
502 } else {
503 style.set_col_width(ch.width);
504 }
505 }
506 }
507 }
508
509 book.attach_sheet(sheet);
510 }
511
512 Ok(())
513}
514
515fn calc_row_header_styles(book: &mut WorkBook) -> Result<(), OdsError> {
517 for i in 0..book.num_sheets() {
518 let mut sheet = book.detach_sheet(i);
519
520 for rh in sheet.row_header.values_mut() {
521 if rh.height != Length::Default && rh.style.is_none() {
522 let rowstyle = book.add_rowstyle(RowStyle::new_empty());
523 rh.style = Some(rowstyle);
524 }
525
526 if let Some(style_name) = rh.style.as_ref() {
527 if let Some(style) = book.rowstyle_mut(style_name) {
528 if rh.height == Length::Default {
529 style.set_use_optimal_row_height(true);
530 style.set_row_height(Length::Default);
531 } else {
532 style.set_row_height(rh.height);
533 }
534 }
535 }
536 }
537
538 book.attach_sheet(sheet);
539 }
540
541 Ok(())
542}
543
544fn calc_metadata(book: &mut WorkBook) -> Result<(), OdsError> {
546 book.metadata.generator = format!("spreadsheet-ods {}", env!("CARGO_PKG_VERSION"));
548 book.metadata.document_statistics.table_count = book.sheets.len() as u32;
549 let mut cell_count = 0;
550 for sheet in book.iter_sheets() {
551 cell_count += sheet.data.len() as u32;
552 }
553 book.metadata.document_statistics.cell_count = cell_count;
554
555 Ok(())
556}
557
558#[allow(clippy::collapsible_else_if)]
561#[allow(clippy::collapsible_if)]
562fn calc_config(book: &mut WorkBook) -> Result<(), OdsError> {
563 let mut config = book.config.detach(0);
565
566 let bc = config.create_path(&[
567 ("ooo:view-settings", ConfigItemType::Set),
568 ("Views", ConfigItemType::Vec),
569 ("0", ConfigItemType::Entry),
570 ]);
571 if book.config().active_table.is_empty() {
572 book.config_mut().active_table = book.sheet(0).name().clone();
573 }
574 bc.insert("ActiveTable", book.config().active_table.clone());
575 bc.insert("HasSheetTabs", book.config().has_sheet_tabs);
576 bc.insert("ShowGrid", book.config().show_grid);
577 bc.insert("ShowPageBreaks", book.config().show_page_breaks);
578
579 for i in 0..book.num_sheets() {
580 let sheet = book.detach_sheet(i);
581
582 let bc = config.create_path(&[
583 ("ooo:view-settings", ConfigItemType::Set),
584 ("Views", ConfigItemType::Vec),
585 ("0", ConfigItemType::Entry),
586 ("Tables", ConfigItemType::Map),
587 (sheet.name().as_str(), ConfigItemType::Entry),
588 ]);
589
590 bc.insert("CursorPositionX", sheet.config().cursor_x);
591 bc.insert("CursorPositionY", sheet.config().cursor_y);
592 bc.insert("HorizontalSplitMode", sheet.config().hor_split_mode as i16);
593 bc.insert("VerticalSplitMode", sheet.config().vert_split_mode as i16);
594 bc.insert("HorizontalSplitPosition", sheet.config().hor_split_pos);
595 bc.insert("VerticalSplitPosition", sheet.config().vert_split_pos);
596 bc.insert("ActiveSplitRange", sheet.config().active_split_range);
597 bc.insert("PositionLeft", sheet.config().position_left);
598 bc.insert("PositionRight", sheet.config().position_right);
599 bc.insert("PositionTop", sheet.config().position_top);
600 bc.insert("PositionBottom", sheet.config().position_bottom);
601 bc.insert("ZoomType", sheet.config().zoom_type);
602 bc.insert("ZoomValue", sheet.config().zoom_value);
603 bc.insert("PageViewZoomValue", sheet.config().page_view_zoom_value);
604 bc.insert("ShowGrid", sheet.config().show_grid);
605
606 let bc = config.create_path(&[
607 ("ooo:configuration-settings", ConfigItemType::Set),
608 ("ScriptConfiguration", ConfigItemType::Map),
609 (sheet.name().as_str(), ConfigItemType::Entry),
610 ]);
611 bc.insert("CodeName", sheet.name().as_str().to_string());
615
616 book.attach_sheet(sheet);
617 }
618
619 book.config.attach(config);
620
621 Ok(())
622}
623
624fn create_manifest(book: &mut WorkBook) -> Result<(), OdsError> {
626 if !book.manifest.contains_key("/") {
627 book.add_manifest(Manifest {
628 full_path: "/".to_string(),
629 version: Some(book.version().clone()),
630 media_type: "application/vnd.oasis.opendocument.spreadsheet".to_string(),
631 buffer: None,
632 });
633 }
634 if !book.manifest.contains_key("manifest.rdf") {
635 book.add_manifest(create_manifest_rdf()?);
636 }
637 if !book.manifest.contains_key("styles.xml") {
638 book.add_manifest(Manifest::new("styles.xml", "text/xml"));
639 }
640 if !book.manifest.contains_key("meta.xml") {
641 book.add_manifest(Manifest::new("meta.xml", "text/xml"));
642 }
643 if !book.manifest.contains_key("content.xml") {
644 book.add_manifest(Manifest::new("content.xml", "text/xml"));
645 }
646 if !book.manifest.contains_key("settings.xml") {
647 book.add_manifest(Manifest::new("settings.xml", "text/xml"));
648 }
649
650 Ok(())
651}
652
653fn create_manifest_rdf() -> Result<Manifest, OdsError> {
654 let mut buf = Vec::new();
655 let mut xml_out = XmlWriter::new(&mut buf);
656
657 xml_out.dtd("UTF-8")?;
658 xml_out.elem("rdf:RDF")?;
659 xml_out.attr_str("xmlns:rdf", "http://www.w3.org/1999/02/22-rdf-syntax-ns#")?;
660 xml_out.elem("rdf:Description")?;
661 xml_out.attr_str("rdf:about", "content.xml")?;
662 xml_out.empty("rdf:type")?;
663 xml_out.attr_str(
664 "rdf:resource",
665 "http://docs.oasis-open.org/ns/office/1.2/meta/odf#ContentFile",
666 )?;
667 xml_out.end_elem("rdf:Description")?;
668 xml_out.elem("rdf:Description")?;
669 xml_out.attr_str("rdf:about", "")?;
670 xml_out.empty("ns0:hasPart")?;
671 xml_out.attr_str(
672 "xmlns:ns0",
673 "http://docs.oasis-open.org/ns/office/1.2/meta/pkg#",
674 )?;
675 xml_out.attr_str("rdf:resource", "content.xml")?;
676 xml_out.end_elem("rdf:Description")?;
677 xml_out.elem("rdf:Description")?;
678 xml_out.attr_str("rdf:about", "")?;
679 xml_out.empty("rdf:type")?;
680 xml_out.attr_str(
681 "rdf:resource",
682 "http://docs.oasis-open.org/ns/office/1.2/meta/pkg#Document",
683 )?;
684 xml_out.end_elem("rdf:Description")?;
685 xml_out.end_elem("rdf:RDF")?;
686 xml_out.close()?;
687
688 Ok(Manifest::with_buf(
689 "manifest.rdf",
690 "application/rdf+xml",
691 buf,
692 ))
693}
694
695fn write_ods_mimetype(write: &'_ mut dyn Write) -> Result<(), io::Error> {
696 write.write_all("application/vnd.oasis.opendocument.spreadsheet".as_bytes())?;
697 Ok(())
698}
699
700fn write_ods_manifest(book: &WorkBook, xml_out: &mut OdsXmlWriter<'_>) -> Result<(), OdsError> {
701 xml_out.dtd("UTF-8")?;
702
703 xml_out.elem("manifest:manifest")?;
704 xml_out.attr_str(
705 "xmlns:manifest",
706 "urn:oasis:names:tc:opendocument:xmlns:manifest:1.0",
707 )?;
708 xml_out.attr_esc("manifest:version", &book.version())?;
709
710 for manifest in book.manifest.values() {
711 xml_out.empty("manifest:file-entry")?;
712 xml_out.attr_esc("manifest:full-path", &manifest.full_path)?;
713 if let Some(version) = &manifest.version {
714 xml_out.attr_esc("manifest:version", version)?;
715 }
716 xml_out.attr_esc("manifest:media-type", &manifest.media_type)?;
717 }
718
719 xml_out.end_elem("manifest:manifest")?;
720
721 xml_out.close()?;
722
723 Ok(())
724}
725
726fn write_xmlns(xmlns: &NamespaceMap, xml_out: &mut OdsXmlWriter<'_>) -> Result<(), OdsError> {
727 for (k, v) in xmlns.entries() {
728 match k {
729 Cow::Borrowed(k) => {
730 xml_out.attr(k, v.as_ref())?;
731 }
732 Cow::Owned(k) => {
733 xml_out.attr_esc(k, v.as_ref())?;
734 }
735 }
736 }
737 Ok(())
738}
739
740fn write_ods_metadata(book: &mut WorkBook, xml_out: &mut OdsXmlWriter<'_>) -> Result<(), OdsError> {
741 let xmlns = book
742 .xmlns
743 .entry("meta.xml".into())
744 .or_insert_with(NamespaceMap::new);
745
746 xmlns.insert_str(
747 "xmlns:meta",
748 "urn:oasis:names:tc:opendocument:xmlns:meta:1.0",
749 );
750 xmlns.insert_str(
751 "xmlns:office",
752 "urn:oasis:names:tc:opendocument:xmlns:office:1.0",
753 );
754
755 xml_out.dtd("UTF-8")?;
756
757 xml_out.elem("office:document-meta")?;
758 write_xmlns(xmlns, xml_out)?;
759 xml_out.attr_esc("office:version", book.version())?;
760
761 write_office_meta(book, xml_out)?;
762
763 xml_out.end_elem("office:document-meta")?;
764
765 xml_out.close()?;
766
767 Ok(())
768}
769
770fn write_office_meta(book: &WorkBook, xml_out: &mut OdsXmlWriter<'_>) -> Result<(), OdsError> {
771 xml_out.elem("office:meta")?;
772
773 xml_out.elem_text("meta:generator", &book.metadata.generator)?;
774 if !book.metadata.title.is_empty() {
775 xml_out.elem_text_esc("dc:title", &book.metadata.title)?;
776 }
777 if !book.metadata.description.is_empty() {
778 xml_out.elem_text_esc("dc:description", &book.metadata.description)?;
779 }
780 if !book.metadata.subject.is_empty() {
781 xml_out.elem_text_esc("dc:subject", &book.metadata.subject)?;
782 }
783 if !book.metadata.keyword.is_empty() {
784 xml_out.elem_text_esc("meta:keyword", &book.metadata.keyword)?;
785 }
786 if !book.metadata.initial_creator.is_empty() {
787 xml_out.elem_text_esc("meta:initial-creator", &book.metadata.initial_creator)?;
788 }
789 if !book.metadata.creator.is_empty() {
790 xml_out.elem_text_esc("dc:creator", &book.metadata.creator)?;
791 }
792 if !book.metadata.printed_by.is_empty() {
793 xml_out.elem_text_esc("meta:printed-by", &book.metadata.printed_by)?;
794 }
795 if let Some(v) = book.metadata.creation_date {
796 xml_out.elem_text("meta:creation-date", &v.format(DATETIME_FORMAT))?;
797 }
798 if let Some(v) = book.metadata.date {
799 xml_out.elem_text("dc:date", &v.format(DATETIME_FORMAT))?;
800 }
801 if let Some(v) = book.metadata.print_date {
802 xml_out.elem_text("meta:print-date", &v.format(DATETIME_FORMAT))?;
803 }
804 if !book.metadata.language.is_empty() {
805 xml_out.elem_text_esc("dc:language", &book.metadata.language)?;
806 }
807 if book.metadata.editing_cycles > 0 {
808 xml_out.elem_text("meta:editing-cycles", &book.metadata.editing_cycles)?;
809 }
810 if book.metadata.editing_duration.num_seconds() > 0 {
811 xml_out.elem_text(
812 "meta:editing-duration",
813 &format_duration2(book.metadata.editing_duration),
814 )?;
815 }
816
817 if !book.metadata.template.is_empty() {
818 xml_out.empty("meta:template")?;
819 if let Some(v) = book.metadata.template.date {
820 xml_out.attr("meta:date", &v.format(DATETIME_FORMAT))?;
821 }
822 if let Some(v) = book.metadata.template.actuate {
823 xml_out.attr("xlink:actuate", &v)?;
824 }
825 if let Some(v) = &book.metadata.template.href {
826 xml_out.attr_esc("xlink:href", v)?;
827 }
828 if let Some(v) = &book.metadata.template.title {
829 xml_out.attr_esc("xlink:title", v)?;
830 }
831 if let Some(v) = book.metadata.template.link_type {
832 xml_out.attr("xlink:type", &v)?;
833 }
834 }
835
836 if !book.metadata.auto_reload.is_empty() {
837 xml_out.empty("meta:auto_reload")?;
838 if let Some(v) = book.metadata.auto_reload.delay {
839 xml_out.attr("meta:delay", &format_duration2(v))?;
840 }
841 if let Some(v) = book.metadata.auto_reload.actuate {
842 xml_out.attr("xlink:actuate", &v)?;
843 }
844 if let Some(v) = &book.metadata.auto_reload.href {
845 xml_out.attr_esc("xlink:href", v)?;
846 }
847 if let Some(v) = &book.metadata.auto_reload.show {
848 xml_out.attr("xlink:show", v)?;
849 }
850 if let Some(v) = book.metadata.auto_reload.link_type {
851 xml_out.attr("xlink:type", &v)?;
852 }
853 }
854
855 if !book.metadata.hyperlink_behaviour.is_empty() {
856 xml_out.empty("meta:hyperlink-behaviour")?;
857 if let Some(v) = &book.metadata.hyperlink_behaviour.target_frame_name {
858 xml_out.attr_esc("office:target-frame-name", v)?;
859 }
860 if let Some(v) = &book.metadata.hyperlink_behaviour.show {
861 xml_out.attr("xlink:show", v)?;
862 }
863 }
864
865 xml_out.empty("meta:document-statistic")?;
866 xml_out.attr(
867 "meta:table-count",
868 &book.metadata.document_statistics.table_count,
869 )?;
870 xml_out.attr(
871 "meta:cell-count",
872 &book.metadata.document_statistics.cell_count,
873 )?;
874 xml_out.attr(
875 "meta:object-count",
876 &book.metadata.document_statistics.object_count,
877 )?;
878 xml_out.attr(
879 "meta:ole-object-count",
880 &book.metadata.document_statistics.ole_object_count,
881 )?;
882
883 for userdef in &book.metadata.user_defined {
884 xml_out.elem("meta:user-defined")?;
885 xml_out.attr("meta:name", &userdef.name)?;
886 if !matches!(userdef.value, MetaValue::String(_)) {
887 xml_out.attr_str(
888 "meta:value-type",
889 match &userdef.value {
890 MetaValue::Boolean(_) => "boolean",
891 MetaValue::Datetime(_) => "date",
892 MetaValue::Float(_) => "float",
893 MetaValue::TimeDuration(_) => "time",
894 MetaValue::String(_) => unreachable!(),
895 },
896 )?;
897 }
898 match &userdef.value {
899 MetaValue::Boolean(v) => xml_out.text_str(if *v { "true" } else { "false" })?,
900 MetaValue::Datetime(v) => xml_out.text(&v.format(DATETIME_FORMAT))?,
901 MetaValue::Float(v) => xml_out.text(&v)?,
902 MetaValue::TimeDuration(v) => xml_out.text(&format_duration2(*v))?,
903 MetaValue::String(v) => xml_out.text_esc(v)?,
904 }
905 xml_out.end_elem("meta:user-defined")?;
906 }
907
908 xml_out.end_elem("office:meta")?;
909 Ok(())
910}
911
912fn write_ods_settings(book: &mut WorkBook, xml_out: &mut OdsXmlWriter<'_>) -> Result<(), OdsError> {
913 let xmlns = book
914 .xmlns
915 .entry("settings.xml".into())
916 .or_insert_with(NamespaceMap::new);
917
918 xmlns.insert_str(
919 "xmlns:office",
920 "urn:oasis:names:tc:opendocument:xmlns:office:1.0",
921 );
922 xmlns.insert_str("xmlns:ooo", "http://openoffice.org/2004/office");
923 xmlns.insert_str(
924 "xmlns:config",
925 "urn:oasis:names:tc:opendocument:xmlns:config:1.0",
926 );
927
928 xml_out.dtd("UTF-8")?;
929
930 xml_out.elem("office:document-settings")?;
931 write_xmlns(xmlns, xml_out)?;
932 xml_out.attr_esc("office:version", book.version())?;
933
934 write_office_settings(book, xml_out)?;
935
936 xml_out.end_elem("office:document-settings")?;
937
938 xml_out.close()?;
939
940 Ok(())
941}
942
943fn write_office_settings(book: &WorkBook, xml_out: &mut OdsXmlWriter<'_>) -> Result<(), OdsError> {
944 xml_out.elem("office:settings")?;
945
946 for (name, item) in book.config.iter() {
947 match item {
948 ConfigItem::Value(_) => {
949 panic!("office-settings must not contain config-item");
950 }
951 ConfigItem::Set(_) => write_config_item_set(name, item, xml_out)?,
952 ConfigItem::Vec(_) => {
953 panic!("office-settings must not contain config-item-map-index")
954 }
955 ConfigItem::Map(_) => {
956 panic!("office-settings must not contain config-item-map-named")
957 }
958 ConfigItem::Entry(_) => {
959 panic!("office-settings must not contain config-item-map-entry")
960 }
961 }
962 }
963
964 xml_out.end_elem("office:settings")?;
965 Ok(())
966}
967
968fn write_config_item_set(
969 name: &str,
970 set: &ConfigItem,
971 xml_out: &mut OdsXmlWriter<'_>,
972) -> Result<(), OdsError> {
973 xml_out.elem("config:config-item-set")?;
974 xml_out.attr_esc("config:name", name)?;
975
976 for (name, item) in set.iter() {
977 match item {
978 ConfigItem::Value(value) => write_config_item(name, value, xml_out)?,
979 ConfigItem::Set(_) => write_config_item_set(name, item, xml_out)?,
980 ConfigItem::Vec(_) => write_config_item_map_indexed(name, item, xml_out)?,
981 ConfigItem::Map(_) => write_config_item_map_named(name, item, xml_out)?,
982 ConfigItem::Entry(_) => {
983 panic!("config-item-set must not contain config-item-map-entry")
984 }
985 }
986 }
987
988 xml_out.end_elem("config:config-item-set")?;
989
990 Ok(())
991}
992
993fn write_config_item_map_indexed(
994 name: &str,
995 vec: &ConfigItem,
996 xml_out: &mut OdsXmlWriter<'_>,
997) -> Result<(), OdsError> {
998 xml_out.elem("config:config-item-map-indexed")?;
999 xml_out.attr_esc("config:name", name)?;
1000
1001 let mut index = 0;
1002 loop {
1003 let index_str = index.to_string();
1004 if let Some(item) = vec.get(&index_str) {
1005 match item {
1006 ConfigItem::Value(value) => write_config_item(name, value, xml_out)?,
1007 ConfigItem::Set(_) => {
1008 panic!("config-item-map-index must not contain config-item-set")
1009 }
1010 ConfigItem::Vec(_) => {
1011 panic!("config-item-map-index must not contain config-item-map-index")
1012 }
1013 ConfigItem::Map(_) => {
1014 panic!("config-item-map-index must not contain config-item-map-named")
1015 }
1016 ConfigItem::Entry(_) => write_config_item_map_entry(None, item, xml_out)?,
1017 }
1018 } else {
1019 break;
1020 }
1021
1022 index += 1;
1023 }
1024
1025 xml_out.end_elem("config:config-item-map-indexed")?;
1026
1027 Ok(())
1028}
1029
1030fn write_config_item_map_named(
1031 name: &str,
1032 map: &ConfigItem,
1033 xml_out: &mut OdsXmlWriter<'_>,
1034) -> Result<(), OdsError> {
1035 xml_out.elem("config:config-item-map-named")?;
1036 xml_out.attr_esc("config:name", name)?;
1037
1038 for (name, item) in map.iter() {
1039 match item {
1040 ConfigItem::Value(value) => write_config_item(name, value, xml_out)?,
1041 ConfigItem::Set(_) => {
1042 panic!("config-item-map-index must not contain config-item-set")
1043 }
1044 ConfigItem::Vec(_) => {
1045 panic!("config-item-map-index must not contain config-item-map-index")
1046 }
1047 ConfigItem::Map(_) => {
1048 panic!("config-item-map-index must not contain config-item-map-named")
1049 }
1050 ConfigItem::Entry(_) => write_config_item_map_entry(Some(name), item, xml_out)?,
1051 }
1052 }
1053
1054 xml_out.end_elem("config:config-item-map-named")?;
1055
1056 Ok(())
1057}
1058
1059fn write_config_item_map_entry(
1060 name: Option<&String>,
1061 map_entry: &ConfigItem,
1062 xml_out: &mut OdsXmlWriter<'_>,
1063) -> Result<(), OdsError> {
1064 xml_out.elem("config:config-item-map-entry")?;
1065 if let Some(name) = name {
1066 xml_out.attr_esc("config:name", name)?;
1067 }
1068
1069 for (name, item) in map_entry.iter() {
1070 match item {
1071 ConfigItem::Value(value) => write_config_item(name, value, xml_out)?,
1072 ConfigItem::Set(_) => write_config_item_set(name, item, xml_out)?,
1073 ConfigItem::Vec(_) => write_config_item_map_indexed(name, item, xml_out)?,
1074 ConfigItem::Map(_) => write_config_item_map_named(name, item, xml_out)?,
1075 ConfigItem::Entry(_) => {
1076 panic!("config:config-item-map-entry must not contain config-item-map-entry")
1077 }
1078 }
1079 }
1080
1081 xml_out.end_elem("config:config-item-map-entry")?;
1082
1083 Ok(())
1084}
1085
1086fn write_config_item(
1087 name: &str,
1088 value: &ConfigValue,
1089 xml_out: &mut OdsXmlWriter<'_>,
1090) -> Result<(), OdsError> {
1091 let is_empty = match value {
1092 ConfigValue::Base64Binary(t) => t.is_empty(),
1093 ConfigValue::String(t) => t.is_empty(),
1094 _ => false,
1095 };
1096
1097 xml_out.elem_if(!is_empty, "config:config-item")?;
1098
1099 xml_out.attr_esc("config:name", name)?;
1100
1101 match value {
1102 ConfigValue::Base64Binary(v) => {
1103 xml_out.attr_str("config:type", "base64Binary")?;
1104 xml_out.text(v)?;
1105 }
1106 ConfigValue::Boolean(v) => {
1107 xml_out.attr_str("config:type", "boolean")?;
1108 xml_out.text(&v)?;
1109 }
1110 ConfigValue::DateTime(v) => {
1111 xml_out.attr_str("config:type", "datetime")?;
1112 xml_out.text(&v.format(DATETIME_FORMAT))?;
1113 }
1114 ConfigValue::Double(v) => {
1115 xml_out.attr_str("config:type", "double")?;
1116 xml_out.text(&v)?;
1117 }
1118 ConfigValue::Int(v) => {
1119 xml_out.attr_str("config:type", "int")?;
1120 xml_out.text(&v)?;
1121 }
1122 ConfigValue::Long(v) => {
1123 xml_out.attr_str("config:type", "long")?;
1124 xml_out.text(&v)?;
1125 }
1126 ConfigValue::Short(v) => {
1127 xml_out.attr_str("config:type", "short")?;
1128 xml_out.text(&v)?;
1129 }
1130 ConfigValue::String(v) => {
1131 xml_out.attr_str("config:type", "string")?;
1132 xml_out.text_esc(v)?;
1133 }
1134 }
1135
1136 xml_out.end_elem_if(!is_empty, "config:config-item")?;
1137
1138 Ok(())
1139}
1140
1141fn write_ods_styles(book: &mut WorkBook, xml_out: &mut OdsXmlWriter<'_>) -> Result<(), OdsError> {
1142 let xmlns = book
1143 .xmlns
1144 .entry("styles.xml".into())
1145 .or_insert_with(NamespaceMap::new);
1146
1147 xmlns.insert_str(
1148 "xmlns:meta",
1149 "urn:oasis:names:tc:opendocument:xmlns:meta:1.0",
1150 );
1151 xmlns.insert_str(
1152 "xmlns:office",
1153 "urn:oasis:names:tc:opendocument:xmlns:office:1.0",
1154 );
1155 xmlns.insert_str(
1156 "xmlns:fo",
1157 "urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0",
1158 );
1159 xmlns.insert_str("xmlns:ooo", "http://openoffice.org/2004/office");
1160 xmlns.insert_str("xmlns:xlink", "http://www.w3.org/1999/xlink");
1161 xmlns.insert_str("xmlns:dc", "http://purl.org/dc/elements/1.1/");
1162 xmlns.insert_str(
1163 "xmlns:style",
1164 "urn:oasis:names:tc:opendocument:xmlns:style:1.0",
1165 );
1166 xmlns.insert_str(
1167 "xmlns:text",
1168 "urn:oasis:names:tc:opendocument:xmlns:text:1.0",
1169 );
1170 xmlns.insert_str(
1171 "xmlns:dr3d",
1172 "urn:oasis:names:tc:opendocument:xmlns:dr3d:1.0",
1173 );
1174 xmlns.insert_str(
1175 "xmlns:svg",
1176 "urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0",
1177 );
1178 xmlns.insert_str(
1179 "xmlns:chart",
1180 "urn:oasis:names:tc:opendocument:xmlns:chart:1.0",
1181 );
1182 xmlns.insert_str("xmlns:rpt", "http://openoffice.org/2005/report");
1183 xmlns.insert_str(
1184 "xmlns:table",
1185 "urn:oasis:names:tc:opendocument:xmlns:table:1.0",
1186 );
1187 xmlns.insert_str(
1188 "xmlns:number",
1189 "urn:oasis:names:tc:opendocument:xmlns:datastyle:1.0",
1190 );
1191 xmlns.insert_str("xmlns:ooow", "http://openoffice.org/2004/writer");
1192 xmlns.insert_str("xmlns:oooc", "http://openoffice.org/2004/calc");
1193 xmlns.insert_str("xmlns:of", "urn:oasis:names:tc:opendocument:xmlns:of:1.2");
1194 xmlns.insert_str("xmlns:tableooo", "http://openoffice.org/2009/table");
1195 xmlns.insert_str(
1196 "xmlns:calcext",
1197 "urn:org:documentfoundation:names:experimental:calc:xmlns:calcext:1.0",
1198 );
1199 xmlns.insert_str("xmlns:drawooo", "http://openoffice.org/2010/draw");
1200 xmlns.insert_str(
1201 "xmlns:draw",
1202 "urn:oasis:names:tc:opendocument:xmlns:drawing:1.0",
1203 );
1204 xmlns.insert_str(
1205 "xmlns:loext",
1206 "urn:org:documentfoundation:names:experimental:office:xmlns:loext:1.0",
1207 );
1208 xmlns.insert_str(
1209 "xmlns:field",
1210 "urn:openoffice:names:experimental:ooo-ms-interop:xmlns:field:1.0",
1211 );
1212 xmlns.insert_str("xmlns:math", "http://www.w3.org/1998/Math/MathML");
1213 xmlns.insert_str(
1214 "xmlns:form",
1215 "urn:oasis:names:tc:opendocument:xmlns:form:1.0",
1216 );
1217 xmlns.insert_str(
1218 "xmlns:script",
1219 "urn:oasis:names:tc:opendocument:xmlns:script:1.0",
1220 );
1221 xmlns.insert_str("xmlns:dom", "http://www.w3.org/2001/xml-events");
1222 xmlns.insert_str("xmlns:xhtml", "http://www.w3.org/1999/xhtml");
1223 xmlns.insert_str("xmlns:grddl", "http://www.w3.org/2003/g/data-view#");
1224 xmlns.insert_str("xmlns:css3t", "http://www.w3.org/TR/css3-text/");
1225 xmlns.insert_str(
1226 "xmlns:presentation",
1227 "urn:oasis:names:tc:opendocument:xmlns:presentation:1.0",
1228 );
1229
1230 xml_out.dtd("UTF-8")?;
1231
1232 xml_out.elem("office:document-styles")?;
1233 write_xmlns(xmlns, xml_out)?;
1234 xml_out.attr_esc("office:version", book.version())?;
1235
1236 write_office_font_face_decls(book, StyleOrigin::Styles, xml_out)?;
1237 write_office_styles(book, StyleOrigin::Styles, xml_out)?;
1238 write_office_automatic_styles(book, StyleOrigin::Styles, xml_out)?;
1239 write_office_master_styles(book, xml_out)?;
1240
1241 xml_out.end_elem("office:document-styles")?;
1242
1243 xml_out.close()?;
1244
1245 Ok(())
1246}
1247
1248fn write_ods_content(book: &mut WorkBook, xml_out: &mut OdsXmlWriter<'_>) -> Result<(), OdsError> {
1249 let xmlns = book
1250 .xmlns
1251 .entry("content.xml".into())
1252 .or_insert_with(NamespaceMap::new);
1253
1254 xmlns.insert_str(
1255 "xmlns:meta",
1256 "urn:oasis:names:tc:opendocument:xmlns:meta:1.0",
1257 );
1258 xmlns.insert_str(
1259 "xmlns:office",
1260 "urn:oasis:names:tc:opendocument:xmlns:office:1.0",
1261 );
1262 xmlns.insert_str(
1263 "xmlns:fo",
1264 "urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0",
1265 );
1266 xmlns.insert_str("xmlns:ooo", "http://openoffice.org/2004/office");
1267 xmlns.insert_str("xmlns:xlink", "http://www.w3.org/1999/xlink");
1268 xmlns.insert_str("xmlns:dc", "http://purl.org/dc/elements/1.1/");
1269 xmlns.insert_str(
1270 "xmlns:style",
1271 "urn:oasis:names:tc:opendocument:xmlns:style:1.0",
1272 );
1273 xmlns.insert_str(
1274 "xmlns:text",
1275 "urn:oasis:names:tc:opendocument:xmlns:text:1.0",
1276 );
1277 xmlns.insert_str(
1278 "xmlns:draw",
1279 "urn:oasis:names:tc:opendocument:xmlns:drawing:1.0",
1280 );
1281 xmlns.insert_str(
1282 "xmlns:dr3d",
1283 "urn:oasis:names:tc:opendocument:xmlns:dr3d:1.0",
1284 );
1285 xmlns.insert_str(
1286 "xmlns:svg",
1287 "urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0",
1288 );
1289 xmlns.insert_str(
1290 "xmlns:chart",
1291 "urn:oasis:names:tc:opendocument:xmlns:chart:1.0",
1292 );
1293 xmlns.insert_str("xmlns:rpt", "http://openoffice.org/2005/report");
1294 xmlns.insert_str(
1295 "xmlns:table",
1296 "urn:oasis:names:tc:opendocument:xmlns:table:1.0",
1297 );
1298 xmlns.insert_str(
1299 "xmlns:number",
1300 "urn:oasis:names:tc:opendocument:xmlns:datastyle:1.0",
1301 );
1302 xmlns.insert_str("xmlns:ooow", "http://openoffice.org/2004/writer");
1303 xmlns.insert_str("xmlns:oooc", "http://openoffice.org/2004/calc");
1304 xmlns.insert_str("xmlns:of", "urn:oasis:names:tc:opendocument:xmlns:of:1.2");
1305 xmlns.insert_str("xmlns:tableooo", "http://openoffice.org/2009/table");
1306 xmlns.insert_str(
1307 "xmlns:calcext",
1308 "urn:org:documentfoundation:names:experimental:calc:xmlns:calcext:1.0",
1309 );
1310 xmlns.insert_str("xmlns:drawooo", "http://openoffice.org/2010/draw");
1311 xmlns.insert_str(
1312 "xmlns:loext",
1313 "urn:org:documentfoundation:names:experimental:office:xmlns:loext:1.0",
1314 );
1315 xmlns.insert_str(
1316 "xmlns:field",
1317 "urn:openoffice:names:experimental:ooo-ms-interop:xmlns:field:1.0",
1318 );
1319 xmlns.insert_str("xmlns:math", "http://www.w3.org/1998/Math/MathML");
1320 xmlns.insert_str(
1321 "xmlns:form",
1322 "urn:oasis:names:tc:opendocument:xmlns:form:1.0",
1323 );
1324 xmlns.insert_str(
1325 "xmlns:script",
1326 "urn:oasis:names:tc:opendocument:xmlns:script:1.0",
1327 );
1328 xmlns.insert_str("xmlns:dom", "http://www.w3.org/2001/xml-events");
1329 xmlns.insert_str("xmlns:xforms", "http://www.w3.org/2002/xforms");
1330 xmlns.insert_str("xmlns:xsd", "http://www.w3.org/2001/XMLSchema");
1331 xmlns.insert_str("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance");
1332 xmlns.insert_str(
1333 "xmlns:formx",
1334 "urn:openoffice:names:experimental:ooxml-odf-interop:xmlns:form:1.0",
1335 );
1336 xmlns.insert_str("xmlns:xhtml", "http://www.w3.org/1999/xhtml");
1337 xmlns.insert_str("xmlns:grddl", "http://www.w3.org/2003/g/data-view#");
1338 xmlns.insert_str("xmlns:css3t", "http://www.w3.org/TR/css3-text/");
1339 xmlns.insert_str(
1340 "xmlns:presentation",
1341 "urn:oasis:names:tc:opendocument:xmlns:presentation:1.0",
1342 );
1343
1344 xml_out.dtd("UTF-8")?;
1345
1346 xml_out.elem("office:document-content")?;
1347 write_xmlns(xmlns, xml_out)?;
1348 xml_out.attr_esc("office:version", book.version())?;
1349
1350 write_office_scripts(book, xml_out)?;
1351 write_office_font_face_decls(book, StyleOrigin::Content, xml_out)?;
1352 write_office_automatic_styles(book, StyleOrigin::Content, xml_out)?;
1353
1354 write_office_body(book, xml_out)?;
1355
1356 xml_out.end_elem("office:document-content")?;
1357
1358 xml_out.close()?;
1359
1360 Ok(())
1361}
1362
1363fn write_office_body(book: &WorkBook, xml_out: &mut OdsXmlWriter<'_>) -> Result<(), OdsError> {
1364 xml_out.elem("office:body")?;
1365 xml_out.elem("office:spreadsheet")?;
1366
1367 for tag in &book.extra {
1369 if tag.name() == "table:calculation-settings"
1370 || tag.name() == "table:label-ranges"
1371 || tag.name() == "table:tracked-changes"
1372 || tag.name() == "text:alphabetical-index-auto-mark-file"
1373 || tag.name() == "text:dde-connection-decls"
1374 || tag.name() == "text:sequence-decls"
1375 || tag.name() == "text:user-field-decls"
1376 || tag.name() == "text:variable-decls"
1377 {
1378 write_xmltag(tag, xml_out)?;
1379 }
1380 }
1381
1382 write_content_validations(book, xml_out)?;
1383
1384 for sheet in &book.sheets {
1385 write_sheet(book, sheet, xml_out)?;
1386 }
1387
1388 for tag in &book.extra {
1390 if tag.name() == "table:consolidation"
1391 || tag.name() == "table:data-pilot-tables"
1392 || tag.name() == "table:database-ranges"
1393 || tag.name() == "table:dde-links"
1394 || tag.name() == "table:named-expressions"
1395 || tag.name() == "calcext:conditional-formats"
1396 {
1397 write_xmltag(tag, xml_out)?;
1398 }
1399 }
1400
1401 xml_out.end_elem("office:spreadsheet")?;
1402 xml_out.end_elem("office:body")?;
1403 Ok(())
1404}
1405
1406fn write_office_scripts(book: &WorkBook, xml_out: &mut OdsXmlWriter<'_>) -> Result<(), OdsError> {
1407 xml_out.elem_if(!book.scripts.is_empty(), "office:scripts")?;
1408 write_scripts(&book.scripts, xml_out)?;
1409 write_event_listeners(&book.event_listener, xml_out)?;
1410 xml_out.end_elem_if(!book.scripts.is_empty(), "office:scripts")?;
1411 Ok(())
1412}
1413
1414fn write_content_validations(
1415 book: &WorkBook,
1416 xml_out: &mut OdsXmlWriter<'_>,
1417) -> Result<(), OdsError> {
1418 if !book.validations.is_empty() {
1419 xml_out.elem("table:content-validations")?;
1420
1421 for valid in book.validations.values() {
1422 xml_out.elem("table:content-validation")?;
1423 xml_out.attr_esc("table:name", valid.name())?;
1424 xml_out.attr_esc("table:condition", &format_validation_condition(valid))?;
1425 xml_out.attr_str(
1426 "table:allow-empty-cell",
1427 if valid.allow_empty() { "true" } else { "false" },
1428 )?;
1429 xml_out.attr_str(
1430 "table:display-list",
1431 match valid.display() {
1432 ValidationDisplay::NoDisplay => "no",
1433 ValidationDisplay::Unsorted => "unsorted",
1434 ValidationDisplay::SortAscending => "sort-ascending",
1435 },
1436 )?;
1437 xml_out.attr_esc("table:base-cell-address", &valid.base_cell())?;
1438
1439 if let Some(err) = valid.err() {
1440 xml_out.elem_if(err.text().is_some(), "table:error-message")?;
1441 xml_out.attr("table:display", &err.display())?;
1442 xml_out.attr("table:message-type", &err.msg_type())?;
1443 if let Some(title) = err.title() {
1444 xml_out.attr_esc("table:title", title)?;
1445 }
1446 if let Some(text) = err.text() {
1447 write_xmltag(text, xml_out)?;
1448 }
1449 xml_out.end_elem_if(err.text().is_some(), "table:error-message")?;
1450 }
1451 if let Some(err) = valid.help() {
1452 xml_out.elem_if(err.text().is_some(), "table:help-message")?;
1453 xml_out.attr("table:display", &err.display())?;
1454 if let Some(title) = err.title() {
1455 xml_out.attr_esc("table:title", title)?;
1456 }
1457 if let Some(text) = err.text() {
1458 write_xmltag(text, xml_out)?;
1459 }
1460 xml_out.end_elem_if(err.text().is_some(), "table:help-message")?;
1461 }
1462
1463 xml_out.end_elem("table:content-validation")?;
1464 }
1465 xml_out.end_elem("table:content-validations")?;
1466 }
1467
1468 Ok(())
1469}
1470
1471#[derive(Debug)]
1472struct SplitCols {
1473 col: u32,
1474 col_to: u32,
1475 hidden: bool,
1476}
1477
1478impl SplitCols {
1479 fn repeat(&self) -> u32 {
1480 self.col_to - self.col + 1
1481 }
1482}
1483
1484fn split_hidden(ranges: &[CellRange], row: u32, col: u32, repeat: u32, out: &mut Vec<SplitCols>) {
1486 out.clear();
1487 if repeat == 1 {
1488 if ranges.iter().any(|v| v.contains(row, col)) {
1489 let range = SplitCols {
1490 col,
1491 col_to: col,
1492 hidden: true,
1493 };
1494 out.push(range);
1495 } else {
1496 let range = SplitCols {
1497 col,
1498 col_to: col,
1499 hidden: false,
1500 };
1501 out.push(range);
1502 }
1503 } else {
1504 let ranges: Vec<_> = ranges
1505 .iter()
1506 .filter(|v| row >= v.row() && row <= v.to_row())
1507 .collect();
1508
1509 let mut range: Option<SplitCols> = None;
1510 'col_loop: for c in col..col + repeat {
1511 for r in &ranges {
1512 if c >= r.col() && c <= r.to_col() {
1513 if let Some(range) = &mut range {
1514 if range.hidden {
1515 range.col_to = c;
1516 } else {
1517 let v = mem::replace(
1518 range,
1519 SplitCols {
1520 col: c,
1521 col_to: c,
1522 hidden: true,
1523 },
1524 );
1525 out.push(v);
1526 }
1527 } else {
1528 range.replace(SplitCols {
1529 col: c,
1530 col_to: c,
1531 hidden: true,
1532 });
1533 }
1534
1535 continue 'col_loop;
1536 }
1537 }
1538 if let Some(range) = &mut range {
1540 if range.hidden {
1541 let v = mem::replace(
1542 range,
1543 SplitCols {
1544 col: c,
1545 col_to: c,
1546 hidden: false,
1547 },
1548 );
1549 out.push(v);
1550 } else {
1551 range.col_to = c;
1552 }
1553 } else {
1554 range.replace(SplitCols {
1555 col: c,
1556 col_to: c,
1557 hidden: false,
1558 });
1559 }
1560 }
1561
1562 if let Some(range) = range {
1563 out.push(range);
1564 }
1565 }
1566}
1567
1568fn remove_outlived(ranges: &mut Vec<CellRange>, row: u32, col: u32) {
1570 *ranges = ranges
1571 .drain(..)
1572 .filter(|s| !s.out_looped(row, col))
1573 .collect();
1574}
1575
1576fn write_sheet(
1577 book: &WorkBook,
1578 sheet: &Sheet,
1579 xml_out: &mut OdsXmlWriter<'_>,
1580) -> Result<(), OdsError> {
1581 xml_out.elem("table:table")?;
1582 dbg!(&sheet.name);
1583 xml_out.attr_esc("table:name", &sheet.name)?;
1584 if let Some(style) = sheet.style.as_ref() {
1585 xml_out.attr_esc("table:style-name", style.as_str())?;
1586 }
1587 if let Some(print_ranges) = &sheet.print_ranges {
1588 xml_out.attr_esc("table:print-ranges", &format_cellranges(print_ranges))?;
1589 }
1590 if !sheet.print() {
1591 xml_out.attr_str("table:print", "false")?;
1592 }
1593 if !sheet.display() {
1594 xml_out.attr_str("table:display", "false")?;
1595 }
1596
1597 for tag in &sheet.extra {
1598 if tag.name() == "table:title"
1599 || tag.name() == "table:desc"
1600 || tag.name() == "table:table-source"
1601 || tag.name() == "office:dde-source"
1602 || tag.name() == "table:scenario"
1603 || tag.name() == "office:forms"
1604 || tag.name() == "table:shapes"
1605 {
1606 write_xmltag(tag, xml_out)?;
1607 }
1608 }
1609
1610 let max_cell = sheet.used_grid_size();
1611
1612 write_table_columns(sheet, max_cell, xml_out)?;
1613
1614 let mut spans = Vec::<CellRange>::new();
1616 let mut split = Vec::<SplitCols>::new();
1617
1618 let mut first_cell = true;
1620 let mut prev_row: u32 = 0;
1621 let mut prev_row_repeat: u32 = 1;
1622 let mut prev_col: u32 = 0;
1623 let mut row_group_count = 0;
1624 let mut row_header = false;
1625
1626 let mut it = CellDataIter::new(sheet.data.range(..));
1627 while let Some(((cur_row, cur_col), cell)) = it.next() {
1628 let cur_row_repeat = if let Some(row_header) = sheet.row_header.get(&cur_row) {
1630 row_header.repeat
1631 } else {
1632 1
1633 };
1634 let cur_col_repeat = cell.repeat;
1636
1637 let (next_row, next_col, is_last_cell) = if let Some((next_row, next_col)) = it.peek_cell()
1645 {
1646 (next_row, next_col, false)
1647 } else {
1648 (max_cell.0, max_cell.1, true)
1649 };
1650
1651 let forward_delta_row = next_row - cur_row;
1653 let forward_delta_col = if forward_delta_row > 0 {
1656 max_cell.1 - cur_col
1657 } else {
1658 next_col - cur_col
1659 };
1660
1661 let backward_delta_row = cur_row - prev_row;
1663 let backward_delta_col = if backward_delta_row > 0 {
1665 cur_col
1666 } else {
1667 cur_col - prev_col
1668 };
1669
1670 if backward_delta_row > 0 && !first_cell {
1673 write_end_prev_row(
1674 sheet,
1675 prev_row,
1676 prev_row_repeat,
1677 &mut row_group_count,
1678 &mut row_header,
1679 xml_out,
1680 )?;
1681 }
1682
1683 if backward_delta_row > 0 {
1685 if backward_delta_row < prev_row_repeat {
1686 return Err(OdsError::Ods(format!(
1687 "{}: row-repeat of {} for row {} overlaps with the following row.",
1688 sheet.name, prev_row_repeat, prev_row,
1689 )));
1690 }
1691
1692 let mut synth_row_repeat = backward_delta_row - prev_row_repeat;
1695 if first_cell {
1698 synth_row_repeat += 1;
1699 }
1700 let synth_row = cur_row - synth_row_repeat;
1701
1702 if synth_row_repeat > 0 {
1703 write_empty_rows_before(
1704 sheet,
1705 synth_row,
1706 synth_row_repeat,
1707 max_cell,
1708 &mut row_group_count,
1709 &mut row_header,
1710 xml_out,
1711 )?;
1712 }
1713 }
1714
1715 if backward_delta_row > 0 || first_cell {
1718 write_start_current_row(
1719 sheet,
1720 cur_row,
1721 cur_row_repeat,
1722 backward_delta_col,
1723 &mut row_group_count,
1724 &mut row_header,
1725 xml_out,
1726 )?;
1727 }
1728
1729 remove_outlived(&mut spans, cur_row, cur_col);
1731
1732 split_hidden(&spans, cur_row, cur_col, cur_col_repeat, &mut split);
1734
1735 if let Some(span) = cell.extra.as_ref().map(|v| v.span) {
1738 if !split[0].hidden && (span.row_span > 1 || span.col_span > 1) {
1739 spans.push(CellRange::origin_span(cur_row, cur_col, span.into()));
1740 }
1741 }
1742
1743 for s in &split {
1745 write_cell(book, cell, s.hidden, s.repeat(), xml_out)?;
1746 }
1747
1748 if forward_delta_row > 0 {
1750 let synth_delta_col = forward_delta_col.saturating_sub(cur_col_repeat);
1753 if synth_delta_col > 0 {
1754 split_hidden(
1755 &spans,
1756 cur_row,
1757 cur_col + cur_col_repeat,
1758 synth_delta_col,
1759 &mut split,
1760 );
1761 for s in &split {
1762 write_empty_cells(s.hidden, s.repeat(), xml_out)?;
1763 }
1764 }
1765 } else if forward_delta_col > 0 {
1766 if forward_delta_col < cur_col_repeat {
1769 return Err(OdsError::Ods(format!(
1770 "{}: col-repeat of {} for row/col {}/{} overlaps with the following cell.",
1771 sheet.name, cur_col_repeat, cur_row, cur_col,
1772 )));
1773 }
1774 let synth_delta_col = forward_delta_col - cur_col_repeat;
1775 if synth_delta_col > 0 {
1776 split_hidden(
1777 &spans,
1778 cur_row,
1779 cur_col + cur_col_repeat,
1780 synth_delta_col,
1781 &mut split,
1782 );
1783 for s in &split {
1784 write_empty_cells(s.hidden, s.repeat(), xml_out)?;
1785 }
1786 }
1787 }
1788
1789 if is_last_cell {
1792 write_end_last_row(&mut row_group_count, &mut row_header, xml_out)?;
1793 }
1794
1795 first_cell = false;
1796 prev_row = cur_row;
1797 prev_row_repeat = cur_row_repeat;
1798 prev_col = cur_col;
1799 }
1800
1801 xml_out.end_elem("table:table")?;
1802
1803 for tag in &sheet.extra {
1804 if tag.name() == "table:named-expressions" || tag.name() == "calcext:conditional-formats" {
1805 write_xmltag(tag, xml_out)?;
1806 }
1807 }
1808
1809 Ok(())
1810}
1811
1812fn write_empty_cells(
1813 hidden: bool,
1814 repeat: u32,
1815 xml_out: &mut OdsXmlWriter<'_>,
1816) -> Result<(), OdsError> {
1817 if hidden {
1818 xml_out.empty("table:covered-table-cell")?;
1819 } else {
1820 xml_out.empty("table:table-cell")?;
1821 }
1822 if repeat > 1 {
1823 xml_out.attr("table:number-columns-repeated", &repeat)?;
1824 }
1825
1826 Ok(())
1827}
1828
1829fn write_start_current_row(
1830 sheet: &Sheet,
1831 cur_row: u32,
1832 cur_row_repeat: u32,
1833 backward_delta_col: u32,
1834 row_group_count: &mut u32,
1835 row_header: &mut bool,
1836 xml_out: &mut OdsXmlWriter<'_>,
1837) -> Result<(), OdsError> {
1838 for row_group in &sheet.group_rows {
1840 if row_group.from() == cur_row {
1841 *row_group_count += 1;
1842 xml_out.elem("table:table-row-group")?;
1843 if !row_group.display() {
1844 xml_out.attr_str("table:display", "false")?;
1845 }
1846 }
1847 if row_group.from() > cur_row && row_group.from() < cur_row + cur_row_repeat {
1850 *row_group_count += 1;
1851 xml_out.elem("table:table-row-group")?;
1852 if !row_group.display() {
1853 xml_out.attr_str("table:display", "false")?;
1854 }
1855 }
1856 }
1857
1858 if let Some(header_rows) = &sheet.header_rows {
1860 if header_rows.from >= cur_row && header_rows.from < cur_row + cur_row_repeat {
1861 *row_header = true;
1862 }
1863 }
1864 if *row_header {
1865 xml_out.elem("table:table-header-rows")?;
1866 }
1867
1868 xml_out.elem("table:table-row")?;
1870 if let Some(row_header) = sheet.valid_row_header(cur_row) {
1871 if row_header.repeat > 1 {
1872 xml_out.attr_esc("table:number-rows-repeated", &row_header.repeat)?;
1873 }
1874 if let Some(rowstyle) = row_header.style.as_ref() {
1875 xml_out.attr_esc("table:style-name", rowstyle.as_str())?;
1876 }
1877 if let Some(cellstyle) = row_header.cellstyle.as_ref() {
1878 xml_out.attr_esc("table:default-cell-style-name", cellstyle.as_str())?;
1879 }
1880 if row_header.visible != Visibility::Visible {
1881 xml_out.attr_esc("table:visibility", &row_header.visible)?;
1882 }
1883 }
1884
1885 if backward_delta_col > 0 {
1887 xml_out.empty("table:table-cell")?;
1888 if backward_delta_col > 1 {
1889 xml_out.attr_esc("table:number-columns-repeated", &backward_delta_col)?;
1890 }
1891 }
1892
1893 Ok(())
1894}
1895
1896fn write_end_prev_row(
1897 sheet: &Sheet,
1898 last_r: u32,
1899 last_r_repeat: u32,
1900 row_group_count: &mut u32,
1901 row_header: &mut bool,
1902 xml_out: &mut OdsXmlWriter<'_>,
1903) -> Result<(), OdsError> {
1904 xml_out.end_elem("table:table-row")?;
1906 if *row_header {
1907 xml_out.end_elem("table:table-header-rows")?;
1908 }
1909
1910 if let Some(header_rows) = &sheet.header_rows {
1912 if header_rows.to >= last_r && header_rows.to < last_r + last_r_repeat {
1913 *row_header = false;
1914 }
1915 }
1916
1917 for row_group in &sheet.group_rows {
1919 if row_group.to() == last_r {
1920 *row_group_count -= 1;
1921 xml_out.end_elem("table:table-row-group")?;
1922 }
1923 if row_group.to() > last_r && row_group.to() < last_r + last_r_repeat {
1925 *row_group_count -= 1;
1926 xml_out.end_elem("table:table-row-group")?;
1927 }
1928 }
1929
1930 Ok(())
1931}
1932
1933fn write_end_last_row(
1934 row_group_count: &mut u32,
1935 row_header: &mut bool,
1936 xml_out: &mut OdsXmlWriter<'_>,
1937) -> Result<(), OdsError> {
1938 xml_out.end_elem("table:table-row")?;
1940
1941 if *row_header {
1944 *row_header = false;
1945 xml_out.end_elem("table:table-header-rows")?;
1946 }
1947
1948 while *row_group_count > 0 {
1950 *row_group_count -= 1;
1951 xml_out.end_elem("table:table-row-group")?;
1952 }
1953
1954 Ok(())
1955}
1956
1957fn write_empty_rows_before(
1958 sheet: &Sheet,
1959 last_row: u32,
1960 last_row_repeat: u32,
1961 max_cell: (u32, u32),
1962 row_group_count: &mut u32,
1963 row_header: &mut bool,
1964 xml_out: &mut OdsXmlWriter<'_>,
1965) -> Result<(), OdsError> {
1966 if !sheet.group_rows.is_empty() || sheet.header_rows.is_some() {
1968 for r in last_row..last_row + last_row_repeat {
1969 if *row_header {
1970 xml_out.end_elem("table:table-header-rows")?;
1971 }
1972 if let Some(header_rows) = &sheet.header_rows {
1974 if header_rows.to == r {
1975 *row_header = false;
1976 }
1977 }
1978 for row_group in &sheet.group_rows {
1980 if row_group.to() == r {
1981 *row_group_count -= 1;
1982 xml_out.end_elem("table:table-row-group")?;
1983 }
1984 }
1985 for row_group in &sheet.group_rows {
1986 if row_group.from() == r {
1987 *row_group_count += 1;
1988 xml_out.elem("table:table-row-group")?;
1989 if !row_group.display() {
1990 xml_out.attr_str("table:display", "false")?;
1991 }
1992 }
1993 }
1994 if let Some(header_rows) = &sheet.header_rows {
1996 if header_rows.from == r {
1997 *row_header = true;
1998 }
1999 }
2000 if *row_header {
2001 xml_out.elem("table:table-header-rows")?;
2002 }
2003 write_empty_row(sheet, r, 1, max_cell, xml_out)?;
2005 }
2006 } else {
2007 write_empty_row(sheet, last_row, last_row_repeat, max_cell, xml_out)?;
2008 }
2009
2010 Ok(())
2011}
2012
2013fn write_empty_row(
2014 sheet: &Sheet,
2015 cur_row: u32,
2016 row_repeat: u32,
2017 max_cell: (u32, u32),
2018 xml_out: &mut OdsXmlWriter<'_>,
2019) -> Result<(), OdsError> {
2020 xml_out.elem("table:table-row")?;
2021 xml_out.attr("table:number-rows-repeated", &row_repeat)?;
2022 if let Some(row_header) = sheet.valid_row_header(cur_row) {
2023 if let Some(rowstyle) = row_header.style.as_ref() {
2024 xml_out.attr_esc("table:style-name", rowstyle.as_str())?;
2025 }
2026 if let Some(cellstyle) = row_header.cellstyle.as_ref() {
2027 xml_out.attr_esc("table:default-cell-style-name", cellstyle.as_str())?;
2028 }
2029 if row_header.visible != Visibility::Visible {
2030 xml_out.attr_esc("table:visibility", &row_header.visible)?;
2031 }
2032 }
2033
2034 xml_out.empty("table:table-cell")?;
2036 xml_out.attr("table:number-columns-repeated", &max_cell.1)?;
2037
2038 xml_out.end_elem("table:table-row")?;
2039
2040 Ok(())
2041}
2042
2043fn write_table_columns(
2044 sheet: &Sheet,
2045 max_cell: (u32, u32),
2046 xml_out: &mut OdsXmlWriter<'_>,
2047) -> Result<(), OdsError> {
2048 let mut max_col = max_cell.1;
2050 for grp in &sheet.group_cols {
2051 max_col = max(max_col, grp.to + 1);
2052 }
2053 if let Some(header_cols) = &sheet.header_cols {
2054 max_col = max(max_col, header_cols.to + 1);
2055 }
2056
2057 let mut c = 0;
2059 loop {
2060 if c >= max_col {
2061 break;
2062 }
2063
2064 for grp in &sheet.group_cols {
2065 if c == grp.from() {
2066 xml_out.elem("table:table-column-group")?;
2067 if !grp.display() {
2068 xml_out.attr_str("table:display", "false")?;
2069 }
2070 }
2071 }
2072
2073 if let Some(header_cols) = &sheet.header_cols {
2075 if c >= header_cols.from && c <= header_cols.to {
2076 xml_out.elem("table:table-header-columns")?;
2077 }
2078 }
2079
2080 xml_out.empty("table:table-column")?;
2081 let span = if let Some(col_header) = sheet.col_header.get(&c) {
2082 if col_header.span > 1 {
2083 xml_out.attr_esc("table:number-columns-repeated", &col_header.span)?;
2084 }
2085 if let Some(style) = col_header.style.as_ref() {
2086 xml_out.attr_esc("table:style-name", style.as_str())?;
2087 }
2088 if let Some(style) = col_header.cellstyle.as_ref() {
2089 xml_out.attr_esc("table:default-cell-style-name", style.as_str())?;
2090 }
2091 if col_header.visible != Visibility::Visible {
2092 xml_out.attr_esc("table:visibility", &col_header.visible)?;
2093 }
2094
2095 col_header.span
2096 } else {
2097 1
2098 };
2099
2100 if let Some(header_cols) = &sheet.header_cols {
2101 if c >= header_cols.from && c <= header_cols.to {
2102 xml_out.end_elem("table:table-header-columns")?;
2103 }
2104 }
2105
2106 for col_group in &sheet.group_cols {
2107 if c == col_group.to() {
2108 xml_out.end_elem("table:table-column-group")?;
2109 }
2110 }
2111
2112 debug_assert!(span > 0);
2113 c += span;
2114 }
2115
2116 Ok(())
2117}
2118
2119#[allow(clippy::single_char_add_str)]
2120fn write_cell(
2121 book: &WorkBook,
2122 cell: &CellData,
2123 is_hidden: bool,
2124 repeat: u32,
2125 xml_out: &mut OdsXmlWriter<'_>,
2126) -> Result<(), OdsError> {
2127 let tag = if is_hidden {
2128 "table:covered-table-cell"
2129 } else {
2130 "table:table-cell"
2131 };
2132
2133 let has_subs = cell.value != Value::Empty || cell.has_annotation() || cell.has_draw_frames();
2134 xml_out.elem_if(has_subs, tag)?;
2135
2136 if let Some(formula) = &cell.formula {
2137 xml_out.attr_esc("table:formula", formula)?;
2138 }
2139
2140 if repeat > 1 {
2141 xml_out.attr_esc("table:number-columns-repeated", &repeat)?;
2142 }
2143
2144 if let Some(style) = &cell.style {
2146 xml_out.attr_esc("table:style-name", style.as_str())?;
2147 } else if let Some(style) = book.def_style(cell.value.value_type()) {
2148 xml_out.attr_esc("table:style-name", style.as_str())?;
2149 }
2150
2151 if let Some(validation_name) = cell.extra.as_ref().and_then(|v| v.validation_name.as_ref()) {
2153 xml_out.attr_esc("table:content-validation-name", validation_name.as_str())?;
2154 }
2155
2156 if let Some(span) = cell.extra.as_ref().map(|v| v.span) {
2158 if span.row_span > 1 {
2159 xml_out.attr_esc("table:number-rows-spanned", &span.row_span)?;
2160 }
2161 if span.col_span > 1 {
2162 xml_out.attr_esc("table:number-columns-spanned", &span.col_span)?;
2163 }
2164 }
2165 if let Some(span) = cell.extra.as_ref().map(|v| v.matrix_span) {
2166 if span.row_span > 1 {
2167 xml_out.attr_esc("table:number-matrix-rows-spanned", &span.row_span)?;
2168 }
2169 if span.col_span > 1 {
2170 xml_out.attr_esc("table:number-matrix-columns-spanned", &span.col_span)?;
2171 }
2172 }
2173
2174 match &cell.value {
2185 Value::Empty => {}
2186 Value::Text(s) => {
2187 xml_out.attr_str("office:value-type", "string")?;
2188 for l in s.split('\n') {
2189 xml_out.elem_text_esc("text:p", l)?;
2190 }
2191 }
2192 Value::TextXml(t) => {
2193 xml_out.attr_str("office:value-type", "string")?;
2194 for tt in t.iter() {
2195 write_xmltag(tt, xml_out)?;
2196 }
2197 }
2198 Value::DateTime(d) => {
2199 xml_out.attr_str("office:value-type", "date")?;
2200 let value = d.format(DATETIME_FORMAT);
2201 xml_out.attr("office:date-value", &value)?;
2202 xml_out.elem("text:p")?;
2203 xml_out.text(&value)?;
2204 xml_out.end_elem("text:p")?;
2205 }
2206 Value::TimeDuration(d) => {
2207 xml_out.attr_str("office:value-type", "time")?;
2208 let value = format_duration2(*d);
2209 xml_out.attr("office:time-value", &value)?;
2210 xml_out.elem("text:p")?;
2211 xml_out.text(&value)?;
2212 xml_out.end_elem("text:p")?;
2213 }
2214 Value::Boolean(b) => {
2215 xml_out.attr_str("office:value-type", "boolean")?;
2216 xml_out.attr_str("office:boolean-value", if *b { "true" } else { "false" })?;
2217 xml_out.elem("text:p")?;
2218 xml_out.text_str(if *b { "true" } else { "false" })?;
2219 xml_out.end_elem("text:p")?;
2220 }
2221 Value::Currency(v, c) => {
2222 xml_out.attr_str("office:value-type", "currency")?;
2223 xml_out.attr_esc("office:currency", c)?;
2224 xml_out.attr("office:value", v)?;
2225 xml_out.elem("text:p")?;
2226 xml_out.text_esc(c)?;
2227 xml_out.text_str(" ")?;
2228 xml_out.text(v)?;
2229 xml_out.end_elem("text:p")?;
2230 }
2231 Value::Number(v) => {
2232 xml_out.attr_str("office:value-type", "float")?;
2233 xml_out.attr("office:value", v)?;
2234 xml_out.elem("text:p")?;
2235 xml_out.text(v)?;
2236 xml_out.end_elem("text:p")?;
2237 }
2238 Value::Percentage(v) => {
2239 xml_out.attr_str("office:value-type", "percentage")?;
2240 xml_out.attr("office:value", v)?;
2241 xml_out.elem("text:p")?;
2242 xml_out.text(v)?;
2243 xml_out.end_elem("text:p")?;
2244 }
2245 }
2246
2247 if let Some(annotation) = cell.extra.as_ref().and_then(|v| v.annotation.as_ref()) {
2248 write_annotation(annotation, xml_out)?;
2249 }
2250
2251 if let Some(draw_frames) = cell.extra.as_ref().map(|v| &v.draw_frames) {
2252 for draw_frame in draw_frames {
2253 write_draw_frame(draw_frame, xml_out)?;
2254 }
2255 }
2256
2257 xml_out.end_elem_if(has_subs, tag)?;
2258
2259 Ok(())
2260}
2261
2262fn write_draw_frame(
2263 draw_frame: &DrawFrame,
2264 xml_out: &mut OdsXmlWriter<'_>,
2265) -> Result<(), OdsError> {
2266 xml_out.elem("draw:frame")?;
2267 for (k, v) in draw_frame.attrmap().iter() {
2268 xml_out.attr_esc(k.as_ref(), v)?;
2269 }
2270
2271 for content in draw_frame.content_ref() {
2272 match content {
2273 DrawFrameContent::Image(img) => {
2274 write_draw_image(img, xml_out)?;
2275 }
2276 }
2277 }
2278
2279 if let Some(desc) = draw_frame.desc() {
2280 xml_out.elem("svg:desc")?;
2281 xml_out.text_esc(desc)?;
2282 xml_out.end_elem("svg:desc")?;
2283 }
2284 if let Some(title) = draw_frame.title() {
2285 xml_out.elem("svg:title")?;
2286 xml_out.text_esc(title)?;
2287 xml_out.end_elem("svg:title")?;
2288 }
2289
2290 xml_out.end_elem("draw:frame")?;
2291
2292 Ok(())
2293}
2294
2295fn write_draw_image(
2296 draw_image: &DrawImage,
2297 xml_out: &mut OdsXmlWriter<'_>,
2298) -> Result<(), OdsError> {
2299 xml_out.elem("draw:image")?;
2300 for (k, v) in draw_image.attrmap().iter() {
2301 xml_out.attr_esc(k.as_ref(), v)?;
2302 }
2303
2304 if let Some(bin) = draw_image.get_binary_base64() {
2305 xml_out.elem("office:binary-data")?;
2306 xml_out.text(bin)?;
2307 xml_out.end_elem("office:binary-data")?;
2308 }
2309
2310 for content in draw_image.get_text() {
2311 write_xmltag(content, xml_out)?;
2312 }
2313
2314 xml_out.end_elem("draw:image")?;
2315
2316 Ok(())
2317}
2318
2319fn write_annotation(
2320 annotation: &Annotation,
2321 xml_out: &mut OdsXmlWriter<'_>,
2322) -> Result<(), OdsError> {
2323 xml_out.elem("office:annotation")?;
2324 xml_out.attr("office:display", &annotation.display())?;
2325 xml_out.attr_esc("office:name", &annotation.name())?;
2326 for (k, v) in annotation.attrmap().iter() {
2327 xml_out.attr_esc(k.as_ref(), v)?;
2328 }
2329
2330 if let Some(creator) = annotation.creator() {
2331 xml_out.elem("dc:creator")?;
2332 xml_out.text_esc(creator.as_str())?;
2333 xml_out.end_elem("dc:creator")?;
2334 }
2335 if let Some(date) = annotation.date() {
2336 xml_out.elem("dc:date")?;
2337 xml_out.text_esc(&date.format(DATETIME_FORMAT))?;
2338 xml_out.end_elem("dc:date")?;
2339 }
2340 for v in annotation.text() {
2341 write_xmltag(v, xml_out)?;
2342 }
2343 xml_out.end_elem("office:annotation")?;
2344 Ok(())
2345}
2346
2347fn write_scripts(scripts: &Vec<Script>, xml_out: &mut OdsXmlWriter<'_>) -> Result<(), OdsError> {
2348 for script in scripts {
2349 xml_out.elem("office:script")?;
2350 xml_out.attr_esc("script:language", &script.script_lang)?;
2351
2352 for content in &script.script {
2353 write_xmlcontent(content, xml_out)?;
2354 }
2355
2356 xml_out.end_elem("/office:script")?;
2357 }
2358
2359 Ok(())
2360}
2361
2362fn write_event_listeners(
2363 events: &HashMap<String, EventListener>,
2364 xml_out: &mut OdsXmlWriter<'_>,
2365) -> Result<(), OdsError> {
2366 for event in events.values() {
2367 xml_out.empty("script:event-listener")?;
2368 xml_out.attr_esc("script:event-name", &event.event_name)?;
2369 xml_out.attr_esc("script:language", &event.script_lang)?;
2370 xml_out.attr_esc("script:macro-name", &event.macro_name)?;
2371 xml_out.attr_esc("xlink:actuate", &event.actuate)?;
2372 xml_out.attr_esc("xlink:href", &event.href)?;
2373 xml_out.attr_esc("xlink:type", &event.link_type)?;
2374 }
2375
2376 Ok(())
2377}
2378
2379fn write_office_font_face_decls(
2380 book: &WorkBook,
2381 origin: StyleOrigin,
2382 xml_out: &mut OdsXmlWriter<'_>,
2383) -> Result<(), OdsError> {
2384 xml_out.elem_if(!book.fonts.is_empty(), "office:font-face-decls")?;
2385 write_style_font_face(&book.fonts, origin, xml_out)?;
2386 xml_out.end_elem_if(!book.fonts.is_empty(), "office:font-face-decls")?;
2387 Ok(())
2388}
2389
2390fn write_style_font_face(
2391 fonts: &HashMap<String, FontFaceDecl>,
2392 origin: StyleOrigin,
2393 xml_out: &mut OdsXmlWriter<'_>,
2394) -> Result<(), OdsError> {
2395 for font in fonts.values().filter(|s| s.origin() == origin) {
2396 xml_out.empty("style:font-face")?;
2397 xml_out.attr_esc("style:name", font.name())?;
2398 for (a, v) in font.attrmap().iter() {
2399 xml_out.attr_esc(a.as_ref(), v)?;
2400 }
2401 }
2402 Ok(())
2403}
2404
2405fn write_office_styles(
2406 book: &WorkBook,
2407 origin: StyleOrigin,
2408 xml_out: &mut OdsXmlWriter<'_>,
2409) -> Result<(), OdsError> {
2410 xml_out.elem("office:styles")?;
2411 write_styles(book, origin, StyleUse::Default, xml_out)?;
2412 write_styles(book, origin, StyleUse::Named, xml_out)?;
2413 write_valuestyles(book, origin, StyleUse::Named, xml_out)?;
2414 write_valuestyles(book, origin, StyleUse::Default, xml_out)?;
2415 xml_out.end_elem("office:styles")?;
2416 Ok(())
2417}
2418
2419fn write_office_automatic_styles(
2420 book: &WorkBook,
2421 origin: StyleOrigin,
2422 xml_out: &mut OdsXmlWriter<'_>,
2423) -> Result<(), OdsError> {
2424 xml_out.elem("office:automatic-styles")?;
2425 write_pagestyles(&book.pagestyles, xml_out)?;
2426 write_styles(book, origin, StyleUse::Automatic, xml_out)?;
2427 write_valuestyles(book, origin, StyleUse::Automatic, xml_out)?;
2428 xml_out.end_elem("office:automatic-styles")?;
2429 Ok(())
2430}
2431
2432fn write_office_master_styles(
2433 book: &WorkBook,
2434 xml_out: &mut OdsXmlWriter<'_>,
2435) -> Result<(), OdsError> {
2436 xml_out.elem("office:master-styles")?;
2437 write_masterpage(&book.masterpages, xml_out)?;
2438 xml_out.end_elem("office:master-styles")?;
2439 Ok(())
2440}
2441
2442fn write_styles(
2443 book: &WorkBook,
2444 origin: StyleOrigin,
2445 styleuse: StyleUse,
2446 xml_out: &mut OdsXmlWriter<'_>,
2447) -> Result<(), OdsError> {
2448 for style in book.colstyles.values() {
2449 if style.origin() == origin && style.styleuse() == styleuse {
2450 write_colstyle(style, xml_out)?;
2451 }
2452 }
2453 for style in book.rowstyles.values() {
2454 if style.origin() == origin && style.styleuse() == styleuse {
2455 write_rowstyle(style, xml_out)?;
2456 }
2457 }
2458 for style in book.tablestyles.values() {
2459 if style.origin() == origin && style.styleuse() == styleuse {
2460 write_tablestyle(style, xml_out)?;
2461 }
2462 }
2463 for style in book.cellstyles.values() {
2464 if style.origin() == origin && style.styleuse() == styleuse {
2465 write_cellstyle(style, xml_out)?;
2466 }
2467 }
2468 for style in book.paragraphstyles.values() {
2469 if style.origin() == origin && style.styleuse() == styleuse {
2470 write_paragraphstyle(style, xml_out)?;
2471 }
2472 }
2473 for style in book.textstyles.values() {
2474 if style.origin() == origin && style.styleuse() == styleuse {
2475 write_textstyle(style, xml_out)?;
2476 }
2477 }
2478 for style in book.rubystyles.values() {
2479 if style.origin() == origin && style.styleuse() == styleuse {
2480 write_rubystyle(style, xml_out)?;
2481 }
2482 }
2483 for style in book.graphicstyles.values() {
2484 if style.origin() == origin && style.styleuse() == styleuse {
2485 write_graphicstyle(style, xml_out)?;
2486 }
2487 }
2488
2489 Ok(())
2499}
2500
2501fn write_tablestyle(style: &TableStyle, xml_out: &mut OdsXmlWriter<'_>) -> Result<(), OdsError> {
2502 let is_empty = style.tablestyle().is_empty();
2503
2504 if style.styleuse() == StyleUse::Default {
2505 xml_out.elem_if(!is_empty, "style:default-style")?;
2506 } else {
2507 xml_out.elem_if(!is_empty, "style:style")?;
2508 xml_out.attr_esc("style:name", style.name())?;
2509 }
2510 xml_out.attr_str("style:family", "table")?;
2511 for (a, v) in style.attrmap().iter() {
2512 match a.as_ref() {
2513 "style:name" => {}
2514 "style:family" => {}
2515 _ => {
2516 xml_out.attr_esc(a.as_ref(), v)?;
2517 }
2518 }
2519 }
2520
2521 if !style.tablestyle().is_empty() {
2522 xml_out.empty("style:table-properties")?;
2523 for (a, v) in style.tablestyle().iter() {
2524 xml_out.attr_esc(a.as_ref(), v)?;
2525 }
2526 }
2527 if style.styleuse() == StyleUse::Default {
2528 xml_out.end_elem_if(!is_empty, "style:default-style")?;
2529 } else {
2530 xml_out.end_elem_if(!is_empty, "style:style")?;
2531 }
2532
2533 Ok(())
2534}
2535
2536fn write_rowstyle(style: &RowStyle, xml_out: &mut OdsXmlWriter<'_>) -> Result<(), OdsError> {
2537 let is_empty = style.rowstyle().is_empty();
2538
2539 if style.styleuse() == StyleUse::Default {
2540 xml_out.elem_if(!is_empty, "style:default-style")?;
2541 } else {
2542 xml_out.elem_if(!is_empty, "style:style")?;
2543 xml_out.attr_esc("style:name", style.name())?;
2544 }
2545 xml_out.attr_str("style:family", "table-row")?;
2546 for (a, v) in style.attrmap().iter() {
2547 match a.as_ref() {
2548 "style:name" => {}
2549 "style:family" => {}
2550 _ => {
2551 xml_out.attr_esc(a.as_ref(), v)?;
2552 }
2553 }
2554 }
2555
2556 if !style.rowstyle().is_empty() {
2557 xml_out.empty("style:table-row-properties")?;
2558 for (a, v) in style.rowstyle().iter() {
2559 xml_out.attr_esc(a.as_ref(), v)?;
2560 }
2561 }
2562 if style.styleuse() == StyleUse::Default {
2563 xml_out.end_elem_if(!is_empty, "style:default-style")?;
2564 } else {
2565 xml_out.end_elem_if(!is_empty, "style:style")?;
2566 }
2567
2568 Ok(())
2569}
2570
2571fn write_colstyle(style: &ColStyle, xml_out: &mut OdsXmlWriter<'_>) -> Result<(), OdsError> {
2572 let is_empty = style.colstyle().is_empty();
2573
2574 if style.styleuse() == StyleUse::Default {
2575 xml_out.elem_if(!is_empty, "style:default-style")?;
2576 } else {
2577 xml_out.elem_if(!is_empty, "style:style")?;
2578 xml_out.attr_esc("style:name", style.name())?;
2579 }
2580 xml_out.attr_str("style:family", "table-column")?;
2581 for (a, v) in style.attrmap().iter() {
2582 match a.as_ref() {
2583 "style:name" => {}
2584 "style:family" => {}
2585 _ => {
2586 xml_out.attr_esc(a.as_ref(), v)?;
2587 }
2588 }
2589 }
2590
2591 if !style.colstyle().is_empty() {
2592 xml_out.empty("style:table-column-properties")?;
2593 for (a, v) in style.colstyle().iter() {
2594 xml_out.attr_esc(a.as_ref(), v)?;
2595 }
2596 }
2597 if style.styleuse() == StyleUse::Default {
2598 xml_out.end_elem_if(!is_empty, "style:default-style")?;
2599 } else {
2600 xml_out.end_elem_if(!is_empty, "style:style")?;
2601 }
2602
2603 Ok(())
2604}
2605
2606fn write_cellstyle(style: &CellStyle, xml_out: &mut OdsXmlWriter<'_>) -> Result<(), OdsError> {
2607 let is_empty = style.cellstyle().is_empty()
2608 && style.paragraphstyle().is_empty()
2609 && style.textstyle().is_empty()
2610 && style.stylemaps().is_none();
2611
2612 if style.styleuse() == StyleUse::Default {
2613 xml_out.elem_if(!is_empty, "style:default-style")?;
2614 } else {
2615 xml_out.elem_if(!is_empty, "style:style")?;
2616 xml_out.attr_esc("style:name", style.name())?;
2617 }
2618 xml_out.attr_str("style:family", "table-cell")?;
2619 for (a, v) in style.attrmap().iter() {
2620 match a.as_ref() {
2621 "style:name" => {}
2622 "style:family" => {}
2623 _ => {
2624 xml_out.attr_esc(a.as_ref(), v)?;
2625 }
2626 }
2627 }
2628
2629 if !style.cellstyle().is_empty() {
2630 xml_out.empty("style:table-cell-properties")?;
2631 for (a, v) in style.cellstyle().iter() {
2632 xml_out.attr_esc(a.as_ref(), v)?;
2633 }
2634 }
2635 if !style.paragraphstyle().is_empty() {
2636 xml_out.empty("style:paragraph-properties")?;
2637 for (a, v) in style.paragraphstyle().iter() {
2638 xml_out.attr_esc(a.as_ref(), v)?;
2639 }
2640 }
2641 if !style.textstyle().is_empty() {
2642 xml_out.empty("style:text-properties")?;
2643 for (a, v) in style.textstyle().iter() {
2644 xml_out.attr_esc(a.as_ref(), v)?;
2645 }
2646 }
2647 if let Some(stylemaps) = style.stylemaps() {
2648 for sm in stylemaps {
2649 xml_out.empty("style:map")?;
2650 xml_out.attr_esc("style:condition", sm.condition())?;
2651 xml_out.attr_esc("style:apply-style-name", sm.applied_style().as_str())?;
2652 if let Some(r) = sm.base_cell() {
2653 xml_out.attr_esc("style:base-cell-address", r)?;
2654 }
2655 }
2656 }
2657 if style.styleuse() == StyleUse::Default {
2658 xml_out.end_elem_if(!is_empty, "style:default-style")?;
2659 } else {
2660 xml_out.end_elem_if(!is_empty, "style:style")?;
2661 }
2662
2663 Ok(())
2664}
2665
2666fn write_paragraphstyle(
2667 style: &ParagraphStyle,
2668 xml_out: &mut OdsXmlWriter<'_>,
2669) -> Result<(), OdsError> {
2670 let is_empty = style.paragraphstyle().is_empty() && style.textstyle().is_empty();
2671
2672 if style.styleuse() == StyleUse::Default {
2673 xml_out.elem_if(!is_empty, "style:default-style")?;
2674 } else {
2675 xml_out.elem_if(!is_empty, "style:style")?;
2676 xml_out.attr_esc("style:name", style.name())?;
2677 }
2678 xml_out.attr_str("style:family", "paragraph")?;
2679 for (a, v) in style.attrmap().iter() {
2680 match a.as_ref() {
2681 "style:name" => {}
2682 "style:family" => {}
2683 _ => {
2684 xml_out.attr_esc(a.as_ref(), v)?;
2685 }
2686 }
2687 }
2688
2689 if !style.paragraphstyle().is_empty() {
2690 if style.tabstops().is_none() {
2691 xml_out.empty("style:paragraph-properties")?;
2692 for (a, v) in style.paragraphstyle().iter() {
2693 xml_out.attr_esc(a.as_ref(), v)?;
2694 }
2695 } else {
2696 xml_out.elem("style:paragraph-properties")?;
2697 for (a, v) in style.paragraphstyle().iter() {
2698 xml_out.attr_esc(a.as_ref(), v)?;
2699 }
2700 xml_out.elem("style:tab-stops")?;
2701 if let Some(tabstops) = style.tabstops() {
2702 for ts in tabstops {
2703 xml_out.empty("style:tab-stop")?;
2704 for (a, v) in ts.attrmap().iter() {
2705 xml_out.attr_esc(a.as_ref(), v)?;
2706 }
2707 }
2708 }
2709 xml_out.end_elem("style:tab-stops")?;
2710 xml_out.end_elem("style:paragraph-properties")?;
2711 }
2712 }
2713 if !style.textstyle().is_empty() {
2714 xml_out.empty("style:text-properties")?;
2715 for (a, v) in style.textstyle().iter() {
2716 xml_out.attr_esc(a.as_ref(), v)?;
2717 }
2718 }
2719 if style.styleuse() == StyleUse::Default {
2720 xml_out.end_elem_if(!is_empty, "style:default-style")?;
2721 } else {
2722 xml_out.end_elem_if(!is_empty, "style:style")?;
2723 }
2724
2725 Ok(())
2726}
2727
2728fn write_textstyle(style: &TextStyle, xml_out: &mut OdsXmlWriter<'_>) -> Result<(), OdsError> {
2729 let is_empty = style.textstyle().is_empty();
2730
2731 if style.styleuse() == StyleUse::Default {
2732 xml_out.elem_if(!is_empty, "style:default-style")?;
2733 } else {
2734 xml_out.elem_if(!is_empty, "style:style")?;
2735 xml_out.attr_esc("style:name", style.name())?;
2736 }
2737 xml_out.attr_str("style:family", "text")?;
2738 for (a, v) in style.attrmap().iter() {
2739 match a.as_ref() {
2740 "style:name" => {}
2741 "style:family" => {}
2742 _ => {
2743 xml_out.attr_esc(a.as_ref(), v)?;
2744 }
2745 }
2746 }
2747
2748 if !style.textstyle().is_empty() {
2749 xml_out.empty("style:text-properties")?;
2750 for (a, v) in style.textstyle().iter() {
2751 xml_out.attr_esc(a.as_ref(), v)?;
2752 }
2753 }
2754 if style.styleuse() == StyleUse::Default {
2755 xml_out.end_elem_if(!is_empty, "style:default-style")?;
2756 } else {
2757 xml_out.end_elem_if(!is_empty, "style:style")?;
2758 }
2759
2760 Ok(())
2761}
2762
2763fn write_rubystyle(style: &RubyStyle, xml_out: &mut OdsXmlWriter<'_>) -> Result<(), OdsError> {
2764 let is_empty = style.rubystyle().is_empty();
2765
2766 if style.styleuse() == StyleUse::Default {
2767 xml_out.elem_if(!is_empty, "style:default-style")?;
2768 } else {
2769 xml_out.elem_if(!is_empty, "style:style")?;
2770 xml_out.attr_esc("style:name", style.name())?;
2771 }
2772 xml_out.attr_str("style:family", "ruby")?;
2773 for (a, v) in style.attrmap().iter() {
2774 match a.as_ref() {
2775 "style:name" => {}
2776 "style:family" => {}
2777 _ => {
2778 xml_out.attr_esc(a.as_ref(), v)?;
2779 }
2780 }
2781 }
2782
2783 if !style.rubystyle().is_empty() {
2784 xml_out.empty("style:ruby-properties")?;
2785 for (a, v) in style.rubystyle().iter() {
2786 xml_out.attr_esc(a.as_ref(), v)?;
2787 }
2788 }
2789 if style.styleuse() == StyleUse::Default {
2790 xml_out.end_elem_if(!is_empty, "style:default-style")?;
2791 } else {
2792 xml_out.end_elem_if(!is_empty, "style:style")?;
2793 }
2794
2795 Ok(())
2796}
2797
2798fn write_graphicstyle(
2799 style: &GraphicStyle,
2800 xml_out: &mut OdsXmlWriter<'_>,
2801) -> Result<(), OdsError> {
2802 let is_empty = style.graphicstyle().is_empty()
2803 && style.paragraphstyle().is_empty()
2804 && style.textstyle().is_empty();
2805
2806 if style.styleuse() == StyleUse::Default {
2807 xml_out.elem_if(!is_empty, "style:default-style")?;
2808 } else {
2809 xml_out.elem_if(!is_empty, "style:style")?;
2810 xml_out.attr_esc("style:name", style.name())?;
2811 }
2812 xml_out.attr_str("style:family", "graphic")?;
2813 for (a, v) in style.attrmap().iter() {
2814 match a.as_ref() {
2815 "style:name" => {}
2816 "style:family" => {}
2817 _ => {
2818 xml_out.attr_esc(a.as_ref(), v)?;
2819 }
2820 }
2821 }
2822
2823 if !style.graphicstyle().is_empty() {
2824 xml_out.empty("style:graphic-properties")?;
2825 for (a, v) in style.graphicstyle().iter() {
2826 xml_out.attr_esc(a.as_ref(), v)?;
2827 }
2828 }
2829 if !style.paragraphstyle().is_empty() {
2830 xml_out.empty("style:paragraph-properties")?;
2831 for (a, v) in style.paragraphstyle().iter() {
2832 xml_out.attr_esc(a.as_ref(), v)?;
2833 }
2834 }
2835 if !style.textstyle().is_empty() {
2836 xml_out.empty("style:text-properties")?;
2837 for (a, v) in style.textstyle().iter() {
2838 xml_out.attr_esc(a.as_ref(), v)?;
2839 }
2840 }
2841
2842 if style.styleuse() == StyleUse::Default {
2843 xml_out.end_elem_if(!is_empty, "style:default-style")?;
2844 } else {
2845 xml_out.end_elem_if(!is_empty, "style:style")?;
2846 }
2847
2848 Ok(())
2849}
2850
2851fn write_valuestyles(
2852 book: &WorkBook,
2853 origin: StyleOrigin,
2854 style_use: StyleUse,
2855 xml_out: &mut OdsXmlWriter<'_>,
2856) -> Result<(), OdsError> {
2857 write_valuestyle(&book.formats_boolean, origin, style_use, xml_out)?;
2858 write_valuestyle(&book.formats_currency, origin, style_use, xml_out)?;
2859 write_valuestyle(&book.formats_datetime, origin, style_use, xml_out)?;
2860 write_valuestyle(&book.formats_number, origin, style_use, xml_out)?;
2861 write_valuestyle(&book.formats_percentage, origin, style_use, xml_out)?;
2862 write_valuestyle(&book.formats_text, origin, style_use, xml_out)?;
2863 write_valuestyle(&book.formats_timeduration, origin, style_use, xml_out)?;
2864 Ok(())
2865}
2866
2867fn write_valuestyle<T: ValueFormatTrait>(
2868 value_formats: &HashMap<String, T>,
2869 origin: StyleOrigin,
2870 styleuse: StyleUse,
2871 xml_out: &mut OdsXmlWriter<'_>,
2872) -> Result<(), OdsError> {
2873 for value_format in value_formats
2874 .values()
2875 .filter(|s| s.origin() == origin && s.styleuse() == styleuse)
2876 {
2877 let tag = match value_format.value_type() {
2878 ValueType::Empty => unreachable!(),
2879 ValueType::Boolean => "number:boolean-style",
2880 ValueType::Number => "number:number-style",
2881 ValueType::Text => "number:text-style",
2882 ValueType::TextXml => "number:text-style",
2883 ValueType::TimeDuration => "number:time-style",
2884 ValueType::Percentage => "number:percentage-style",
2885 ValueType::Currency => "number:currency-style",
2886 ValueType::DateTime => "number:date-style",
2887 };
2888
2889 xml_out.elem(tag)?;
2890 xml_out.attr_esc("style:name", value_format.name())?;
2891 for (a, v) in value_format.attrmap().iter() {
2892 xml_out.attr_esc(a.as_ref(), v)?;
2893 }
2894
2895 if !value_format.textstyle().is_empty() {
2896 xml_out.empty("style:text-properties")?;
2897 for (a, v) in value_format.textstyle().iter() {
2898 xml_out.attr_esc(a.as_ref(), v)?;
2899 }
2900 }
2901
2902 for part in value_format.parts() {
2903 let part_tag = match part.part_type() {
2904 FormatPartType::Boolean => "number:boolean",
2905 FormatPartType::Number => "number:number",
2906 FormatPartType::ScientificNumber => "number:scientific-number",
2907 FormatPartType::CurrencySymbol => "number:currency-symbol",
2908 FormatPartType::Day => "number:day",
2909 FormatPartType::Month => "number:month",
2910 FormatPartType::Year => "number:year",
2911 FormatPartType::Era => "number:era",
2912 FormatPartType::DayOfWeek => "number:day-of-week",
2913 FormatPartType::WeekOfYear => "number:week-of-year",
2914 FormatPartType::Quarter => "number:quarter",
2915 FormatPartType::Hours => "number:hours",
2916 FormatPartType::Minutes => "number:minutes",
2917 FormatPartType::Seconds => "number:seconds",
2918 FormatPartType::Fraction => "number:fraction",
2919 FormatPartType::AmPm => "number:am-pm",
2920 FormatPartType::Text => "number:text",
2921 FormatPartType::TextContent => "number:text-content",
2922 FormatPartType::FillCharacter => "number:fill-character",
2923 };
2924
2925 if part.part_type() == FormatPartType::Text
2926 || part.part_type() == FormatPartType::CurrencySymbol
2927 || part.part_type() == FormatPartType::FillCharacter
2928 {
2929 let content = part.content().filter(|v| !v.is_empty());
2930 xml_out.elem_if(content.is_some(), part_tag)?;
2931 for (a, v) in part.attrmap().iter() {
2932 xml_out.attr_esc(a.as_ref(), v)?;
2933 }
2934 if let Some(content) = content {
2935 xml_out.text_esc(content)?;
2936 }
2937 xml_out.end_elem_if(content.is_some(), part_tag)?;
2938 } else if part.part_type() == FormatPartType::Number {
2939 if let Some(position) = part.position() {
2940 xml_out.elem(part_tag)?;
2941 for (a, v) in part.attrmap().iter() {
2942 xml_out.attr_esc(a.as_ref(), v)?;
2943 }
2944
2945 if let Some(content) = part.content() {
2947 xml_out.elem("number:embedded-text")?;
2948 xml_out.attr_esc("number:position", &position)?;
2949 xml_out.text_esc(content)?;
2950 xml_out.end_elem("number:embedded-text")?;
2951 } else {
2952 xml_out.empty("number:embedded-text")?;
2953 xml_out.attr_esc("number:position", &position)?;
2954 }
2955
2956 xml_out.end_elem(part_tag)?;
2957 } else {
2958 xml_out.empty(part_tag)?;
2959 for (a, v) in part.attrmap().iter() {
2960 xml_out.attr_esc(a.as_ref(), v)?;
2961 }
2962 }
2963 } else {
2964 xml_out.empty(part_tag)?;
2965 for (a, v) in part.attrmap().iter() {
2966 xml_out.attr_esc(a.as_ref(), v)?;
2967 }
2968 }
2969 }
2970
2971 if let Some(stylemaps) = value_format.stylemaps() {
2972 for sm in stylemaps {
2973 xml_out.empty("style:map")?;
2974 xml_out.attr_esc("style:condition", sm.condition())?;
2975 xml_out.attr_esc("style:apply-style-name", sm.applied_style())?;
2976 }
2977 }
2978
2979 xml_out.end_elem(tag)?;
2980 }
2981
2982 Ok(())
2983}
2984
2985fn write_pagestyles(
2986 styles: &HashMap<PageStyleRef, PageStyle>,
2987 xml_out: &mut OdsXmlWriter<'_>,
2988) -> Result<(), OdsError> {
2989 for style in styles.values() {
2990 xml_out.elem("style:page-layout")?;
2991 xml_out.attr_esc("style:name", style.name())?;
2992 if let Some(master_page_usage) = &style.master_page_usage {
2993 xml_out.attr_esc("style:page-usage", master_page_usage)?;
2994 }
2995
2996 if !style.style().is_empty() {
2997 xml_out.empty("style:page-layout-properties")?;
2998 for (k, v) in style.style().iter() {
2999 xml_out.attr_esc(k.as_ref(), v)?;
3000 }
3001 }
3002
3003 xml_out.elem("style:header-style")?;
3004 xml_out.empty("style:header-footer-properties")?;
3005 if !style.headerstyle().style().is_empty() {
3006 for (k, v) in style.headerstyle().style().iter() {
3007 xml_out.attr_esc(k.as_ref(), v)?;
3008 }
3009 }
3010 xml_out.end_elem("style:header-style")?;
3011
3012 xml_out.elem("style:footer-style")?;
3013 xml_out.empty("style:header-footer-properties")?;
3014 if !style.footerstyle().style().is_empty() {
3015 for (k, v) in style.footerstyle().style().iter() {
3016 xml_out.attr_esc(k.as_ref(), v)?;
3017 }
3018 }
3019 xml_out.end_elem("style:footer-style")?;
3020
3021 xml_out.end_elem("style:page-layout")?;
3022 }
3023
3024 Ok(())
3025}
3026
3027fn write_masterpage(
3028 masterpages: &HashMap<MasterPageRef, MasterPage>,
3029 xml_out: &mut OdsXmlWriter<'_>,
3030) -> Result<(), OdsError> {
3031 for masterpage in masterpages.values() {
3032 xml_out.elem("style:master-page")?;
3033 xml_out.attr_esc("style:name", masterpage.name())?;
3034 if !masterpage.display_name().is_empty() {
3035 xml_out.attr_esc("style:display-name", masterpage.display_name())?;
3036 }
3037 if let Some(style) = masterpage.pagestyle() {
3038 xml_out.attr_esc("style:page-layout-name", style.as_str())?;
3039 }
3040 if let Some(next) = masterpage.next_masterpage() {
3041 xml_out.attr_esc("style:next-style-name", next.as_str())?;
3042 }
3043
3044 xml_out.elem_if(!masterpage.header().is_empty(), "style:header")?;
3046 if !masterpage.header().display() {
3047 xml_out.attr_str("style:display", "false")?;
3048 }
3049 write_regions(masterpage.header(), xml_out)?;
3050 xml_out.end_elem_if(!masterpage.header().is_empty(), "style:header")?;
3051
3052 xml_out.elem_if(!masterpage.header_first().is_empty(), "style:header-first")?;
3053 if !masterpage.header_first().display() || masterpage.header_first().is_empty() {
3054 xml_out.attr_str("style:display", "false")?;
3055 }
3056 write_regions(masterpage.header_first(), xml_out)?;
3057 xml_out.end_elem_if(!masterpage.header_first().is_empty(), "style:header-first")?;
3058
3059 xml_out.elem_if(!masterpage.header_left().is_empty(), "style:header-left")?;
3060 if !masterpage.header_left().display() || masterpage.header_left().is_empty() {
3061 xml_out.attr_str("style:display", "false")?;
3062 }
3063 write_regions(masterpage.header_left(), xml_out)?;
3064 xml_out.end_elem_if(!masterpage.header_left().is_empty(), "style:header-left")?;
3065
3066 xml_out.elem_if(!masterpage.footer().is_empty(), "style:footer")?;
3068 if !masterpage.footer().display() {
3069 xml_out.attr_str("style:display", "false")?;
3070 }
3071 write_regions(masterpage.footer(), xml_out)?;
3072 xml_out.end_elem_if(!masterpage.footer().is_empty(), "style:footer")?;
3073
3074 xml_out.elem_if(!masterpage.footer_first().is_empty(), "style:footer-first")?;
3075 if !masterpage.footer_first().display() || masterpage.footer_first().is_empty() {
3076 xml_out.attr_str("style:display", "false")?;
3077 }
3078 write_regions(masterpage.footer_first(), xml_out)?;
3079 xml_out.end_elem_if(!masterpage.footer_first().is_empty(), "style:footer-first")?;
3080
3081 xml_out.elem_if(!masterpage.footer_left().is_empty(), "style:footer-left")?;
3082 if !masterpage.footer_left().display() || masterpage.footer_left().is_empty() {
3083 xml_out.attr_str("style:display", "false")?;
3084 }
3085 write_regions(masterpage.footer_left(), xml_out)?;
3086 xml_out.end_elem_if(!masterpage.footer_left().is_empty(), "style:footer-left")?;
3087
3088 xml_out.end_elem("style:master-page")?;
3089 }
3090
3091 Ok(())
3092}
3093
3094fn write_regions(hf: &HeaderFooter, xml_out: &mut OdsXmlWriter<'_>) -> Result<(), OdsError> {
3095 if !hf.left().is_empty() {
3096 xml_out.elem("style:region-left")?;
3097 for v in hf.left() {
3098 write_xmltag(v, xml_out)?;
3099 }
3100 xml_out.end_elem("style:region-left")?;
3101 }
3102 if !hf.center().is_empty() {
3103 xml_out.elem("style:region-center")?;
3104 for v in hf.center() {
3105 write_xmltag(v, xml_out)?;
3106 }
3107 xml_out.end_elem("style:region-center")?;
3108 }
3109 if !hf.right().is_empty() {
3110 xml_out.elem("style:region-right")?;
3111 for v in hf.right() {
3112 write_xmltag(v, xml_out)?;
3113 }
3114 xml_out.end_elem("style:region-right")?;
3115 }
3116 for content in hf.content() {
3117 write_xmltag(content, xml_out)?;
3118 }
3119
3120 Ok(())
3121}
3122
3123fn write_xmlcontent(x: &XmlContent, xml_out: &mut OdsXmlWriter<'_>) -> Result<(), OdsError> {
3124 match x {
3125 XmlContent::Text(t) => {
3126 xml_out.text_esc(t)?;
3127 }
3128 XmlContent::Tag(t) => {
3129 write_xmltag(t, xml_out)?;
3130 }
3131 }
3132 Ok(())
3133}
3134
3135fn write_xmltag(x: &XmlTag, xml_out: &mut OdsXmlWriter<'_>) -> Result<(), OdsError> {
3136 xml_out.elem_if(!x.is_empty(), x.name())?;
3137
3138 for (k, v) in x.attrmap().iter() {
3139 xml_out.attr_esc(k.as_ref(), v)?;
3140 }
3141
3142 for c in x.content() {
3143 match c {
3144 XmlContent::Text(t) => {
3145 xml_out.text_esc(t)?;
3146 }
3147 XmlContent::Tag(t) => {
3148 write_xmltag(t, xml_out)?;
3149 }
3150 }
3151 }
3152
3153 xml_out.end_elem_if(!x.is_empty(), x.name())?;
3154
3155 Ok(())
3156}
3157
3158fn write_ods_extra<W: Write + Seek>(
3160 cfg: &OdsWriteOptions,
3161 zip_writer: &mut ZipWriter<W>,
3162 book: &WorkBook,
3163) -> Result<(), OdsError> {
3164 for manifest in book.manifest.values() {
3165 if !matches!(
3166 manifest.full_path.as_str(),
3167 "/" | "settings.xml" | "styles.xml" | "content.xml" | "meta.xml"
3168 ) {
3169 if manifest.is_dir() {
3170 zip_writer.add_directory(&manifest.full_path, FileOptions::<()>::default())?;
3171 } else {
3172 zip_writer.start_file(
3173 manifest.full_path.as_str(),
3174 FileOptions::<()>::default()
3175 .compression_method(cfg.method)
3176 .compression_level(cfg.level),
3177 )?;
3178 if let Some(buf) = &manifest.buffer {
3179 zip_writer.write_all(buf.as_slice())?;
3180 }
3181 }
3182 }
3183 }
3184
3185 Ok(())
3186}