Skip to main content

excelize_rs/
stream.rs

1//! Streaming writer API.
2//!
3//! Ported from Go `stream.go`.
4
5use std::collections::HashMap;
6use std::fs;
7use std::fs::File as FsFile;
8use std::io::{self, BufReader, Cursor, Read, Write};
9use std::path::PathBuf;
10use std::thread;
11use std::time::{SystemTime, UNIX_EPOCH};
12
13use chrono::{NaiveDate, NaiveDateTime};
14use quick_xml::Reader;
15use quick_xml::escape::escape;
16use quick_xml::events::Event;
17use quick_xml::se::to_string as xml_to_string;
18use std::time::Duration;
19
20use crate::cell::CellValue;
21use crate::constants::{
22    DEFAULT_COL_WIDTH, MAX_COLUMN_WIDTH, MAX_COLUMNS, MAX_ROW_HEIGHT, MIN_COLUMNS,
23    NAMESPACE_SPREADSHEET, STREAM_CHUNK_SIZE, TOTAL_ROWS, XML_HEADER,
24};
25use crate::date;
26use crate::errors::{
27    ErrColumnNumber, ErrColumnWidth, ErrMaxRowHeight, ErrOutlineLevel, ErrSheetNotExist, Result,
28};
29use crate::file::File;
30use crate::lib_util::{
31    cell_name_to_coordinates, cell_refs_to_coordinates, coordinates_to_cell_name,
32    coordinates_to_range_ref, range_ref_to_coordinates, sort_coordinates,
33};
34use crate::styles::Style;
35use crate::xml::common::{RichTextRun, XlsxT};
36use crate::xml::shared_strings::XlsxSi;
37use crate::xml::table::{
38    Table as TableOptions, XlsxAutoFilter, XlsxTable, XlsxTableColumn, XlsxTableColumns,
39    XlsxTableStyleInfo,
40};
41use crate::xml::worksheet::{
42    Panes, XlsxBreaks, XlsxBrk, XlsxC, XlsxCol, XlsxColBreaks, XlsxCols, XlsxF, XlsxPane,
43    XlsxRowBreaks, XlsxSelection, XlsxSheetViews, XlsxWorksheet,
44};
45
46/// Cell value used by the streaming writer to carry a per-cell style or formula.
47///
48/// Ported from Go `stream.go` `Cell`. The `value` field is optional to match
49/// Go's `interface{}` value which may be `nil`.
50#[derive(Debug, Clone, PartialEq)]
51pub struct Cell {
52    pub style_id: i32,
53    pub formula: String,
54    pub value: Option<CellValue>,
55}
56
57impl Default for Cell {
58    fn default() -> Self {
59        Self {
60            style_id: 0,
61            formula: String::new(),
62            value: None,
63        }
64    }
65}
66
67/// Row formatting options used by `StreamWriter::set_row`.
68#[derive(Debug, Default, Clone, Copy, PartialEq)]
69pub struct RowOpts {
70    pub height: f64,
71    pub hidden: bool,
72    pub style_id: i32,
73    pub outline_level: i32,
74}
75
76/// Internal representation of a value to be written by the streaming writer.
77pub struct StreamCell {
78    style_id: i32,
79    formula: String,
80    value: Option<CellValue>,
81}
82
83/// Values that can be passed to `StreamWriter::set_row`.
84pub trait StreamCellValue {
85    fn to_stream(&self) -> StreamCell;
86}
87
88impl StreamCellValue for CellValue {
89    fn to_stream(&self) -> StreamCell {
90        StreamCell {
91            style_id: 0,
92            formula: String::new(),
93            value: Some(self.clone()),
94        }
95    }
96}
97
98impl StreamCellValue for Cell {
99    fn to_stream(&self) -> StreamCell {
100        StreamCell {
101            style_id: self.style_id,
102            formula: self.formula.clone(),
103            value: self.value.clone(),
104        }
105    }
106}
107
108impl StreamCellValue for &Cell {
109    fn to_stream(&self) -> StreamCell {
110        (*self).to_stream()
111    }
112}
113
114impl<T: StreamCellValue> StreamCellValue for Option<T> {
115    fn to_stream(&self) -> StreamCell {
116        match self {
117            Some(v) => v.to_stream(),
118            None => StreamCell {
119                style_id: 0,
120                formula: String::new(),
121                value: None,
122            },
123        }
124    }
125}
126
127macro_rules! impl_stream_cell_value_for_int {
128    ($t:ty) => {
129        impl StreamCellValue for $t {
130            fn to_stream(&self) -> StreamCell {
131                CellValue::Int(*self as i64).to_stream()
132            }
133        }
134    };
135}
136
137impl_stream_cell_value_for_int!(i8);
138impl_stream_cell_value_for_int!(i16);
139impl_stream_cell_value_for_int!(i32);
140impl_stream_cell_value_for_int!(i64);
141impl_stream_cell_value_for_int!(isize);
142impl_stream_cell_value_for_int!(u8);
143impl_stream_cell_value_for_int!(u16);
144impl_stream_cell_value_for_int!(u32);
145impl_stream_cell_value_for_int!(u64);
146impl_stream_cell_value_for_int!(usize);
147
148impl StreamCellValue for f32 {
149    fn to_stream(&self) -> StreamCell {
150        CellValue::Float(*self as f64).to_stream()
151    }
152}
153
154impl StreamCellValue for f64 {
155    fn to_stream(&self) -> StreamCell {
156        CellValue::Float(*self).to_stream()
157    }
158}
159
160impl StreamCellValue for bool {
161    fn to_stream(&self) -> StreamCell {
162        CellValue::Bool(*self).to_stream()
163    }
164}
165
166impl StreamCellValue for &str {
167    fn to_stream(&self) -> StreamCell {
168        CellValue::String(self.to_string()).to_stream()
169    }
170}
171
172impl StreamCellValue for String {
173    fn to_stream(&self) -> StreamCell {
174        CellValue::String(self.clone()).to_stream()
175    }
176}
177
178impl StreamCellValue for NaiveDateTime {
179    fn to_stream(&self) -> StreamCell {
180        CellValue::DateTime(*self).to_stream()
181    }
182}
183
184impl StreamCellValue for NaiveDate {
185    fn to_stream(&self) -> StreamCell {
186        CellValue::Date(*self).to_stream()
187    }
188}
189
190impl StreamCellValue for chrono::NaiveTime {
191    fn to_stream(&self) -> StreamCell {
192        CellValue::Time(*self).to_stream()
193    }
194}
195
196impl StreamCellValue for Duration {
197    fn to_stream(&self) -> StreamCell {
198        CellValue::Float(self.as_nanos() as f64 / 86_400_000_000_000.0).to_stream()
199    }
200}
201
202impl StreamCellValue for Vec<RichTextRun> {
203    fn to_stream(&self) -> StreamCell {
204        CellValue::RichText(self.clone()).to_stream()
205    }
206}
207
208impl StreamCellValue for num_complex::Complex32 {
209    fn to_stream(&self) -> StreamCell {
210        CellValue::String(self.to_string()).to_stream()
211    }
212}
213
214impl StreamCellValue for num_complex::Complex64 {
215    fn to_stream(&self) -> StreamCell {
216        CellValue::String(self.to_string()).to_stream()
217    }
218}
219
220/// Streaming writer for worksheets with large amounts of data.
221#[derive(Debug)]
222pub struct StreamWriter<'a> {
223    file: &'a File,
224    sheet: String,
225    #[allow(dead_code)]
226    sheet_id: i32,
227    sheet_written: bool,
228    worksheet: XlsxWorksheet,
229    raw_data: BufferedWriter,
230    rows: i32,
231    merge_cells_count: i32,
232    merge_cells: String,
233    table_parts: String,
234    flushed: bool,
235}
236
237impl<'a> StreamWriter<'a> {
238    /// Create a new stream writer for the given worksheet.
239    pub fn new(file: &'a File, sheet: &str) -> Result<Self> {
240        crate::excelize::check_sheet_name(sheet)?;
241        let sheet_id = file.get_sheet_id(sheet);
242        if sheet_id == -1 {
243            return Err(Box::new(ErrSheetNotExist {
244                sheet_name: sheet.to_string(),
245            }));
246        }
247
248        let worksheet = file.work_sheet_reader(sheet)?;
249        let tmp_dir = {
250            let opts = file.options.lock().unwrap();
251            opts.tmp_dir.clone()
252        };
253
254        let mut raw_data = BufferedWriter::new(tmp_dir);
255        raw_data.write_str(&format!(
256            "{XML_HEADER}<worksheet{TEMPLATE_NAMESPACE_ID_MAP}"
257        ))?;
258        bulk_append_fields(&mut raw_data, &worksheet, 3, 4)?;
259
260        Ok(Self {
261            file,
262            sheet: sheet.to_string(),
263            sheet_id,
264            sheet_written: false,
265            worksheet,
266            raw_data,
267            rows: 0,
268            merge_cells_count: 0,
269            merge_cells: String::new(),
270            table_parts: String::new(),
271            flushed: false,
272        })
273    }
274
275    /// Add a table within the streamed sheet.
276    pub fn add_table(&mut self, table: &TableOptions) -> Result<()> {
277        let options = parse_table_options(table)?;
278        let mut coordinates = range_ref_to_coordinates(&options.range).map_err(|e| io_err(e))?;
279        sort_coordinates(&mut coordinates).map_err(|e| io_err(e))?;
280
281        // Correct the minimum number of rows, the table at least two lines.
282        let mut coordinates = coordinates;
283        if coordinates[1] == coordinates[3] {
284            coordinates[3] += 1;
285        }
286
287        let ref_str = coordinates_to_range_ref(&coordinates, false).map_err(|e| io_err(e))?;
288        let table_headers = self.get_row_values(coordinates[1], coordinates[0], coordinates[2])?;
289        let table_columns: Vec<XlsxTableColumn> = table_headers
290            .into_iter()
291            .enumerate()
292            .map(|(i, name)| XlsxTableColumn {
293                id: (i + 1) as i64,
294                name,
295                ..Default::default()
296            })
297            .collect();
298
299        let table_id = self.file.count_tables() + 1;
300        let name = if options.name.is_empty() {
301            format!("Table{table_id}")
302        } else {
303            options.name.clone()
304        };
305
306        let tbl = XlsxTable {
307            xmlns: Some(NAMESPACE_SPREADSHEET.to_string()),
308            id: table_id as i64,
309            name: name.clone(),
310            display_name: Some(name),
311            r#ref: ref_str.clone(),
312            auto_filter: Some(XlsxAutoFilter {
313                r#ref: ref_str,
314                ..Default::default()
315            }),
316            table_columns: Some(XlsxTableColumns {
317                count: table_columns.len() as i64,
318                table_column: table_columns,
319            }),
320            table_style_info: Some(XlsxTableStyleInfo {
321                name: Some(options.style_name.clone()),
322                show_first_column: options.show_first_column,
323                show_last_column: options.show_last_column,
324                show_row_stripes: options.show_row_stripes.unwrap_or(true),
325                show_column_stripes: options.show_column_stripes,
326            }),
327            ..Default::default()
328        };
329
330        let sheet_relationships_table_xml = format!("../tables/table{table_id}.xml");
331        let table_xml = sheet_relationships_table_xml.replace("..", "xl");
332        let sheet_xml_path = self
333            .file
334            .get_sheet_xml_path(&self.sheet)
335            .unwrap_or_default();
336        let sheet_rels = format!(
337            "xl/worksheets/_rels/{}.rels",
338            sheet_xml_path.trim_start_matches("xl/worksheets/")
339        );
340        let r_id = self.file.add_rels(
341            &sheet_rels,
342            crate::constants::SOURCE_RELATIONSHIP_TABLE,
343            &sheet_relationships_table_xml,
344            "",
345        );
346
347        self.table_parts = format!(
348            r#"<tableParts count="1"><tablePart r:id="rId{r_id}"></tablePart></tableParts>"#
349        );
350
351        self.file.add_content_type_part(table_id, "table")?;
352        let mut body = xml_to_string(&tbl)?.into_bytes();
353        crate::file::strip_empty_attributes(&mut body);
354        self.file.save_file_list(&table_xml, &body);
355        Ok(())
356    }
357
358    /// Write a row of values starting at `cell`.
359    ///
360    /// Values may be any type implementing [`StreamCellValue`]. Mixed types can
361    /// be passed as `&[&dyn StreamCellValue]` to match Go's `[]interface{}`.
362    /// Row options are optional and default to [`RowOpts::default`] when `None`.
363    pub fn set_row(
364        &mut self,
365        cell: &str,
366        values: &[&dyn StreamCellValue],
367        opts: Option<RowOpts>,
368    ) -> Result<()> {
369        if self.flushed {
370            return Err(crate::errors::new_stream_set_row_order_error("SetRow").into());
371        }
372        let (col, row) = cell_name_to_coordinates(cell).map_err(|e| io_err(e))?;
373        if row <= self.rows {
374            return Err(crate::errors::new_stream_set_row_error(row).into());
375        }
376        self.rows = row;
377        self.write_sheet_data()?;
378
379        let opts = opts.unwrap_or_default();
380        let attrs = opts.marshal_attrs()?;
381        self.raw_data
382            .write_str(&format!(r#"<row r="{row}"{attrs}>"#))?;
383
384        for (i, val) in values.iter().enumerate() {
385            let val = val.to_stream();
386            // Match Go behavior: skip only values that are nil (None) and have
387            // no per-cell style or formula. Empty strings are written as inline
388            // strings, and styled/formula cells are written even without a value.
389            if val.value.is_none() && val.formula.is_empty() && val.style_id == 0 {
390                continue;
391            }
392            let ref_str =
393                coordinates_to_cell_name(col + i as i32, row, false).map_err(|e| io_err(e))?;
394            let mut c = XlsxC {
395                r: Some(ref_str),
396                s: Some(prepare_cell_style(
397                    &self.worksheet,
398                    (col + i as i32) as i64,
399                    row as i64,
400                    opts.style_id as i64,
401                )),
402                ..Default::default()
403            };
404
405            if !val.formula.is_empty() {
406                set_cell_formula(&mut c, &val.formula);
407            }
408            if val.style_id > 0 {
409                c.s = Some(val.style_id as i64);
410            }
411            if let Some(value) = val.value {
412                if let Err(e) = self.set_cell_val_func(&mut c, value) {
413                    self.raw_data.write_str("</row>")?;
414                    return Err(e);
415                }
416            }
417            write_cell(&mut self.raw_data, &c)?;
418        }
419        self.raw_data.write_str("</row>")?;
420        Ok(self.raw_data.sync()?)
421    }
422
423    /// Set column visibility for a range of columns.
424    pub fn set_col_visible(&mut self, min_val: i32, max_val: i32, visible: bool) -> Result<()> {
425        if self.sheet_written {
426            return Err(crate::errors::new_stream_set_row_order_error("SetColVisible").into());
427        }
428        if min_val < MIN_COLUMNS
429            || min_val > MAX_COLUMNS
430            || max_val < MIN_COLUMNS
431            || max_val > MAX_COLUMNS
432        {
433            return Err(Box::new(ErrColumnNumber));
434        }
435        let (min_val, max_val) = if min_val > max_val {
436            (max_val, min_val)
437        } else {
438            (min_val, max_val)
439        };
440        set_col_visible_ws(&mut self.worksheet, min_val, max_val, visible);
441        Ok(())
442    }
443
444    /// Set the outline level for a column.
445    pub fn set_col_outline_level(&mut self, col: i32, level: u8) -> Result<()> {
446        if self.sheet_written {
447            return Err(crate::errors::new_stream_set_row_order_error("SetColOutlineLevel").into());
448        }
449        if col < MIN_COLUMNS || col > MAX_COLUMNS {
450            return Err(Box::new(ErrColumnNumber));
451        }
452        if level == 0 || level > 7 {
453            return Err(Box::new(ErrOutlineLevel));
454        }
455        set_col_outline_level_ws(&mut self.worksheet, col, level);
456        Ok(())
457    }
458
459    /// Set the style for a range of columns.
460    pub fn set_col_style(&mut self, min_val: i32, max_val: i32, style_id: i32) -> Result<()> {
461        if self.sheet_written {
462            return Err(crate::errors::new_stream_set_row_order_error("SetColStyle").into());
463        }
464        if min_val < MIN_COLUMNS
465            || min_val > MAX_COLUMNS
466            || max_val < MIN_COLUMNS
467            || max_val > MAX_COLUMNS
468        {
469            return Err(Box::new(ErrColumnNumber));
470        }
471        let (min_val, max_val) = if max_val < min_val {
472            (max_val, min_val)
473        } else {
474            (min_val, max_val)
475        };
476        let styles = self.file.styles_reader()?;
477        let style_count = styles.cell_xfs.as_ref().map(|x| x.xf.len()).unwrap_or(1) as i32;
478        if style_id < 0 || style_id >= style_count {
479            return Err(crate::errors::new_invalid_style_id_error(style_id).into());
480        }
481        set_col_style_ws(&mut self.worksheet, min_val, max_val, style_id);
482        Ok(())
483    }
484
485    /// Set the width for a range of columns.
486    pub fn set_col_width(&mut self, min_val: i32, max_val: i32, width: f64) -> Result<()> {
487        if self.sheet_written {
488            return Err(crate::errors::new_stream_set_row_order_error("SetColWidth").into());
489        }
490        if min_val < MIN_COLUMNS
491            || min_val > MAX_COLUMNS
492            || max_val < MIN_COLUMNS
493            || max_val > MAX_COLUMNS
494        {
495            return Err(Box::new(ErrColumnNumber));
496        }
497        if width > MAX_COLUMN_WIDTH as f64 {
498            return Err(Box::new(ErrColumnWidth));
499        }
500        let (min_val, max_val) = if min_val > max_val {
501            (max_val, min_val)
502        } else {
503            (min_val, max_val)
504        };
505        set_col_width_ws(&mut self.worksheet, min_val, max_val, width);
506        Ok(())
507    }
508
509    /// Insert a page break at the given cell.
510    pub fn insert_page_break(&mut self, cell: &str) -> Result<()> {
511        insert_page_break_ws(&mut self.worksheet, cell)
512    }
513
514    /// Set panes for the streamed sheet.
515    pub fn set_panes(&mut self, panes: &Panes) -> Result<()> {
516        if self.sheet_written {
517            return Err(crate::errors::new_stream_set_row_order_error("SetPanes").into());
518        }
519        set_panes_ws(&mut self.worksheet, panes)
520    }
521
522    /// Merge cells in the streamed sheet.
523    pub fn merge_cell(&mut self, top_left_cell: &str, bottom_right_cell: &str) -> Result<()> {
524        cell_refs_to_coordinates(top_left_cell, bottom_right_cell).map_err(|e| io_err(e))?;
525        self.merge_cells_count += 1;
526        self.merge_cells.push_str(&format!(
527            r#"<mergeCell ref="{}:{}"/>"#,
528            top_left_cell, bottom_right_cell
529        ));
530        Ok(())
531    }
532
533    /// Flush the streaming writer.
534    pub fn flush(&mut self) -> Result<()> {
535        if self.flushed {
536            return Ok(());
537        }
538        self.write_sheet_data()?;
539        self.raw_data.write_str("</sheetData>")?;
540        bulk_append_fields(&mut self.raw_data, &self.worksheet, 9, 16)?;
541
542        if self.merge_cells_count > 0 {
543            self.raw_data.write_str(&format!(
544                r#"<mergeCells count="{}">{}</mergeCells>"#,
545                self.merge_cells_count, self.merge_cells
546            ))?;
547        }
548
549        bulk_append_fields(&mut self.raw_data, &self.worksheet, 18, 39)?;
550        self.raw_data.write_str(&self.table_parts)?;
551        bulk_append_fields(&mut self.raw_data, &self.worksheet, 41, 41)?;
552        self.raw_data.write_str("</worksheet>")?;
553        self.raw_data.flush()?;
554
555        let tmp_path = self.raw_data.into_temp_file()?;
556        let sheet_path = self
557            .file
558            .get_sheet_xml_path(&self.sheet)
559            .unwrap_or_default();
560        self.file.pkg.remove(&sheet_path);
561        self.file.sheet.remove(&sheet_path);
562        self.file.checked.remove(&sheet_path);
563        if let Some(tmp_path) = tmp_path {
564            let state = crate::file::StreamState {
565                tmp_path,
566                sheet_path: sheet_path.clone(),
567            };
568            self.file.streams.borrow_mut().insert(sheet_path, state);
569        }
570        self.flushed = true;
571        Ok(())
572    }
573
574    fn write_sheet_data(&mut self) -> Result<()> {
575        if !self.sheet_written {
576            bulk_append_fields(&mut self.raw_data, &self.worksheet, 5, 6)?;
577            if let Some(cols) = &self.worksheet.cols {
578                self.raw_data.write_str("<cols>")?;
579                for col in &cols.col {
580                    self.raw_data
581                        .write_str(&format!(r#"<col min="{}" max="{}""#, col.min, col.max))?;
582                    if let Some(width) = col.width {
583                        self.raw_data
584                            .write_str(&format!(r#" width="{width}" customWidth="1""#))?;
585                    }
586                    if let Some(style) = col.style {
587                        self.raw_data.write_str(&format!(r#" style="{style}""#))?;
588                    }
589                    if col.hidden.unwrap_or(false) {
590                        self.raw_data.write_str(r#" hidden="1""#)?;
591                    }
592                    if let Some(level) = col.outline_level {
593                        self.raw_data
594                            .write_str(&format!(r#" outlineLevel="{level}""#))?;
595                    }
596                    if col.collapsed.unwrap_or(false) {
597                        self.raw_data.write_str(r#" collapsed="1""#)?;
598                    }
599                    if col.best_fit.unwrap_or(false) {
600                        self.raw_data.write_str(r#" bestFit="1""#)?;
601                    }
602                    self.raw_data.write_str("/>")?;
603                }
604                self.raw_data.write_str("</cols>")?;
605            }
606            self.raw_data.write_str("<sheetData>")?;
607            self.sheet_written = true;
608        }
609        Ok(())
610    }
611
612    fn set_cell_val_func(&self, c: &mut XlsxC, value: CellValue) -> Result<()> {
613        match value {
614            CellValue::Int(n) => {
615                c.t = None;
616                c.v = Some(n.to_string());
617            }
618            CellValue::Float(n) => {
619                set_cell_float(c, n, -1, 64);
620            }
621            CellValue::String(s) => {
622                set_cell_value(c, &s);
623            }
624            CellValue::Bool(b) => {
625                c.t = Some("b".to_string());
626                c.v = Some(if b { "1".to_string() } else { "0".to_string() });
627            }
628            CellValue::Formula(f) => {
629                set_cell_formula(c, &f);
630            }
631            CellValue::DateTime(dt) => {
632                set_cell_time(self, c, dt, true)?;
633            }
634            CellValue::Date(d) => {
635                set_cell_time(self, c, d.and_hms_opt(0, 0, 0).unwrap_or_default(), false)?;
636            }
637            CellValue::Time(t) => {
638                c.t = None;
639                c.v = Some(format!("{:.15}", date::time_to_excel_serial(t)));
640                if c.s.unwrap_or(0) == 0 {
641                    let style_id = self.file.new_style(&Style {
642                        num_fmt: 21,
643                        ..Default::default()
644                    })?;
645                    c.s = Some(style_id as i64);
646                }
647            }
648            CellValue::Duration(d) => {
649                c.t = None;
650                c.v = Some(format!(
651                    "{}",
652                    format_float(d.as_nanos() as f64 / 86_400_000_000_000.0)
653                ));
654            }
655            CellValue::RichText(runs) => {
656                c.t = Some("inlineStr".to_string());
657                c.is = Some(crate::cell::runs_to_xlsx_si(&runs));
658            }
659        }
660        Ok(())
661    }
662
663    fn get_row_values(&mut self, h_row: i32, h_col: i32, v_col: i32) -> Result<Vec<String>> {
664        let mut res = vec![String::new(); (v_col - h_col + 1) as usize];
665        let sst = self.file.shared_strings_reader().ok();
666        let reader = self.raw_data.reader()?;
667        let mut reader = Reader::from_reader(BufReader::new(reader));
668        let mut buf = Vec::new();
669        let mut in_target = false;
670        let mut in_cell = false;
671        let mut in_v = false;
672        let mut in_is = false;
673        let mut cell_type = None::<String>;
674        let mut current_col = 0;
675        let mut current_val = String::new();
676
677        loop {
678            match reader.read_event_into(&mut buf) {
679                Ok(Event::Start(e)) | Ok(Event::Empty(e)) => {
680                    let local_name = e.local_name();
681                    let name = local_name.as_ref();
682                    if !in_target && name == b"row" {
683                        if let Ok(Some(attr)) = e.try_get_attribute("r") {
684                            if let Ok(r_str) = attr.decode_and_unescape_value(reader.decoder()) {
685                                if r_str.parse::<i32>().unwrap_or(0) == h_row {
686                                    in_target = true;
687                                }
688                            }
689                        }
690                    } else if in_target && name == b"c" {
691                        in_cell = true;
692                        current_col = 0;
693                        cell_type = None;
694                        current_val.clear();
695                        if let Ok(Some(attr)) = e.try_get_attribute("r") {
696                            if let Ok(r_str) = attr.decode_and_unescape_value(reader.decoder()) {
697                                if let Ok((col, _)) = cell_name_to_coordinates(&r_str) {
698                                    current_col = col;
699                                }
700                            }
701                        }
702                        if let Ok(Some(attr)) = e.try_get_attribute("t") {
703                            if let Ok(t) = attr.decode_and_unescape_value(reader.decoder()) {
704                                cell_type = Some(t.to_string());
705                            }
706                        }
707                    } else if in_cell && name == b"v" {
708                        in_v = true;
709                        current_val.clear();
710                    } else if in_cell && name == b"is" {
711                        in_is = true;
712                        current_val.clear();
713                    }
714                }
715                Ok(Event::Text(e)) => {
716                    if in_v || in_is {
717                        if let Ok(text) = e.unescape() {
718                            current_val.push_str(&text);
719                        }
720                    }
721                }
722                Ok(Event::End(e)) => {
723                    let local_name = e.local_name();
724                    let name = local_name.as_ref();
725                    if name == b"row" && in_target {
726                        break;
727                    }
728                    if in_target && name == b"v" {
729                        in_v = false;
730                        let val = if cell_type.as_deref() == Some("s") {
731                            if let Ok(idx) = current_val.parse::<usize>() {
732                                sst.as_ref()
733                                    .and_then(|s| s.si.get(idx))
734                                    .map(|si| extract_si_text(si))
735                                    .unwrap_or_default()
736                            } else {
737                                String::new()
738                            }
739                        } else {
740                            current_val.clone()
741                        };
742                        if current_col >= h_col && current_col <= v_col {
743                            res[(current_col - h_col) as usize] = val;
744                        }
745                    } else if in_target && name == b"is" {
746                        in_is = false;
747                        if current_col >= h_col && current_col <= v_col {
748                            res[(current_col - h_col) as usize] = current_val.clone();
749                        }
750                    } else if in_target && name == b"c" {
751                        in_cell = false;
752                        cell_type = None;
753                    }
754                }
755                Ok(Event::Eof) => break,
756                Err(e) => return Err(Box::new(e)),
757                _ => {}
758            }
759            buf.clear();
760        }
761        Ok(res)
762    }
763}
764
765impl<'a> Drop for StreamWriter<'a> {
766    fn drop(&mut self) {
767        if !self.flushed {
768            let _ = self.flush();
769        }
770    }
771}
772
773impl File {
774    /// Create a new stream writer for the given worksheet.
775    pub fn new_stream_writer(&self, sheet: &str) -> Result<StreamWriter<'_>> {
776        StreamWriter::new(self, sheet)
777    }
778}
779
780impl RowOpts {
781    fn marshal_attrs(&self) -> Result<String> {
782        if self.height > MAX_ROW_HEIGHT as f64 {
783            return Err(Box::new(ErrMaxRowHeight));
784        }
785        if self.outline_level > 7 {
786            return Err(Box::new(ErrOutlineLevel));
787        }
788        let mut attrs = String::new();
789        if self.style_id > 0 {
790            attrs.push_str(&format!(r#" s="{}" customFormat="1""#, self.style_id));
791        }
792        if self.height > 0.0 {
793            attrs.push_str(&format!(
794                r#" ht="{}" customHeight="1""#,
795                format_float(self.height)
796            ));
797        }
798        if self.outline_level > 0 {
799            attrs.push_str(&format!(r#" outlineLevel="{}""#, self.outline_level));
800        }
801        if self.hidden {
802            attrs.push_str(r#" hidden="1""#);
803        }
804        Ok(attrs)
805    }
806}
807
808fn set_cell_formula(c: &mut XlsxC, formula: &str) {
809    if !formula.is_empty() {
810        c.t = Some("str".to_string());
811        c.f = Some(XlsxF {
812            content: formula.to_string(),
813            ..Default::default()
814        });
815    }
816}
817
818fn set_cell_time(
819    sw: &StreamWriter<'_>,
820    c: &mut XlsxC,
821    val: NaiveDateTime,
822    has_time: bool,
823) -> Result<()> {
824    let date1904 = sw
825        .file
826        .workbook_reader()?
827        .workbook_pr
828        .as_ref()
829        .and_then(|p| p.date1904)
830        .unwrap_or(false);
831    let serial = date::datetime_to_excel_serial(val, date1904);
832    let is_num = serial > 0.0;
833    if is_num {
834        c.v = Some(format!("{:.15}", serial));
835        if c.s.unwrap_or(0) == 0 {
836            let num_fmt = if has_time && val.date() != NaiveDate::default() {
837                22
838            } else if has_time {
839                21
840            } else {
841                14
842            };
843            let style_id = sw.file.new_style(&Style {
844                num_fmt,
845                ..Default::default()
846            })?;
847            c.s = Some(style_id as i64);
848        }
849    } else {
850        c.v = Some(val.format("%Y-%m-%dT%H:%M:%S").to_string());
851    }
852    Ok(())
853}
854
855fn set_cell_float(c: &mut XlsxC, value: f64, precision: i32, _bit_size: u32) {
856    if value.is_nan() || value.is_infinite() {
857        c.t = Some("inlineStr".to_string());
858        c.v = Some(String::new());
859        c.is = Some(XlsxSi {
860            t: Some(XlsxT {
861                space: None,
862                val: value.to_string(),
863            }),
864            ..Default::default()
865        });
866        return;
867    }
868    c.t = None;
869    c.v = Some(if precision < 0 {
870        format_float(value)
871    } else {
872        format!("{value:.precision$}", precision = precision as usize)
873    });
874}
875
876fn set_cell_value(c: &mut XlsxC, val: &str) {
877    if c.f.is_some() {
878        c.t = Some("str".to_string());
879        c.v = Some(val.to_string());
880        if needs_space_preserve(val) {
881            c.xml_space = Some("preserve".to_string());
882        }
883    } else {
884        c.t = Some("inlineStr".to_string());
885        c.v = Some(String::new());
886        c.is = Some(XlsxSi {
887            t: Some(XlsxT {
888                space: if needs_space_preserve(val) {
889                    Some("preserve".to_string())
890                } else {
891                    None
892                },
893                val: val.to_string(),
894            }),
895            ..Default::default()
896        });
897    }
898}
899
900fn write_cell(buf: &mut BufferedWriter, c: &XlsxC) -> Result<()> {
901    buf.write_str("<c")?;
902    if let Some(space) = &c.xml_space {
903        buf.write_str(&format!(r#" xml:space="{space}""#))?;
904    }
905    if let Some(r) = &c.r {
906        buf.write_str(&format!(r#" r="{r}""#))?;
907    }
908    if let Some(s) = c.s {
909        if s != 0 {
910            buf.write_str(&format!(r#" s="{s}""#))?;
911        }
912    }
913    if let Some(t) = &c.t {
914        buf.write_str(&format!(r#" t="{t}""#))?;
915    }
916    buf.write_str(">")?;
917
918    if let Some(f) = &c.f {
919        buf.write_str("<f>")?;
920        buf.write_str(&escape_xml(&f.content))?;
921        buf.write_str("</f>")?;
922    }
923    if let Some(v) = &c.v {
924        if !v.is_empty() {
925            buf.write_str("<v>")?;
926            buf.write_str(&escape_xml(v))?;
927            buf.write_str("</v>")?;
928        }
929    }
930    if let Some(is) = &c.is {
931        if !is.r.is_empty() {
932            let runs = xml_to_string(&is.r)?;
933            buf.write_str("<is>")?;
934            buf.write_str(&runs)?;
935            buf.write_str("</is>")?;
936        } else if let Some(t) = &is.t {
937            buf.write_str("<is><t")?;
938            if let Some(space) = &t.space {
939                buf.write_str(&format!(r#" xml:space="{space}""#))?;
940            }
941            buf.write_str(">")?;
942            buf.write_str(&escape_xml(&t.val))?;
943            buf.write_str("</t></is>")?;
944        }
945    }
946    buf.write_str("</c>")?;
947    Ok(())
948}
949
950fn prepare_cell_style(ws: &XlsxWorksheet, col: i64, row: i64, style: i64) -> i64 {
951    if style != 0 {
952        return style;
953    }
954    if row > 0 && row as usize <= ws.sheet_data.row.len() {
955        if let Some(style_id) = ws.sheet_data.row[row as usize - 1].s {
956            if style_id != 0 {
957                return style_id;
958            }
959        }
960    }
961    if let Some(cols) = &ws.cols {
962        for c in &cols.col {
963            if c.min <= col && col <= c.max {
964                if let Some(style_id) = c.style {
965                    if style_id != 0 {
966                        return style_id;
967                    }
968                }
969            }
970        }
971    }
972    style
973}
974
975fn bulk_append_fields<W: Write>(
976    w: &mut W,
977    ws: &XlsxWorksheet,
978    from: usize,
979    to: usize,
980) -> Result<()> {
981    for i in from..=to {
982        match i {
983            3 => append_field_option(w, &ws.sheet_pr)?,
984            4 => append_field_option(w, &ws.dimension)?,
985            5 => append_field_option(w, &ws.sheet_views)?,
986            6 => append_field_option(w, &ws.sheet_format_pr)?,
987            9 => append_field_option(w, &ws.sheet_calc_pr)?,
988            10 => append_field_option(w, &ws.sheet_protection)?,
989            11 => append_field_option(w, &ws.protected_ranges)?,
990            12 => append_field_option(w, &ws.scenarios)?,
991            13 => append_field_option(w, &ws.auto_filter)?,
992            14 => append_field_option(w, &ws.sort_state)?,
993            15 => append_field_option(w, &ws.data_consolidate)?,
994            16 => append_field_option(w, &ws.custom_sheet_views)?,
995            18 => append_field_option(w, &ws.phonetic_pr)?,
996            19 => append_field_vec(w, &ws.conditional_formatting)?,
997            20 => append_field_option(w, &ws.data_validations)?,
998            21 => append_field_option(w, &ws.hyperlinks)?,
999            22 => append_field_option(w, &ws.print_options)?,
1000            23 => append_field_option(w, &ws.page_margins)?,
1001            24 => append_field_option(w, &ws.page_setup)?,
1002            25 => append_field_option(w, &ws.header_footer)?,
1003            26 => append_field_option(w, &ws.row_breaks)?,
1004            27 => append_field_option(w, &ws.col_breaks)?,
1005            28 => append_field_option(w, &ws.custom_properties)?,
1006            29 => append_field_option(w, &ws.cell_watches)?,
1007            30 => append_field_option(w, &ws.ignored_errors)?,
1008            31 => append_field_option(w, &ws.smart_tags)?,
1009            32 => append_field_option(w, &ws.drawing)?,
1010            33 => append_field_option(w, &ws.legacy_drawing)?,
1011            34 => append_field_option(w, &ws.legacy_drawing_hf)?,
1012            35 => append_field_option(w, &ws.drawing_hf)?,
1013            36 => append_field_option(w, &ws.picture)?,
1014            37 => append_field_option(w, &ws.ole_objects)?,
1015            38 => append_field_option(w, &ws.controls)?,
1016            39 => append_field_option(w, &ws.web_publish_items)?,
1017            41 => append_field_option(w, &ws.table_parts)?,
1018            _ => {}
1019        }
1020    }
1021    Ok(())
1022}
1023
1024fn append_field_option<W: Write, T: serde::Serialize>(w: &mut W, opt: &Option<T>) -> Result<()> {
1025    if let Some(v) = opt {
1026        w.write_all(xml_to_string(v)?.as_bytes())?;
1027    }
1028    Ok(())
1029}
1030
1031fn append_field_vec<W: Write, T: serde::Serialize>(w: &mut W, vec: &[T]) -> Result<()> {
1032    if !vec.is_empty() {
1033        for v in vec {
1034            w.write_all(xml_to_string(v)?.as_bytes())?;
1035        }
1036    }
1037    Ok(())
1038}
1039
1040// ------------------------------------------------------------------
1041// Column helpers
1042// ------------------------------------------------------------------
1043
1044fn set_col_visible_ws(ws: &mut XlsxWorksheet, min_val: i32, max_val: i32, visible: bool) {
1045    let col_data = XlsxCol {
1046        min: min_val as i64,
1047        max: max_val as i64,
1048        width: Some(DEFAULT_COL_WIDTH),
1049        hidden: Some(!visible),
1050        custom_width: Some(true),
1051        ..Default::default()
1052    };
1053    ws.cols = Some(XlsxCols {
1054        col: flat_cols(
1055            col_data,
1056            ws.cols.as_ref().map(|c| c.col.as_slice()).unwrap_or(&[]),
1057            |fc, c| {
1058                fc.best_fit = c.best_fit;
1059                fc.collapsed = c.collapsed;
1060                fc.custom_width = c.custom_width;
1061                fc.outline_level = c.outline_level;
1062                fc.phonetic = c.phonetic;
1063                fc.style = c.style;
1064                fc.width = c.width;
1065            },
1066        ),
1067    });
1068}
1069
1070fn set_col_outline_level_ws(ws: &mut XlsxWorksheet, col_num: i32, level: u8) {
1071    let col_data = XlsxCol {
1072        min: col_num as i64,
1073        max: col_num as i64,
1074        outline_level: Some(level),
1075        custom_width: Some(true),
1076        ..Default::default()
1077    };
1078    ws.cols = Some(XlsxCols {
1079        col: flat_cols(
1080            col_data,
1081            ws.cols.as_ref().map(|c| c.col.as_slice()).unwrap_or(&[]),
1082            |fc, c| {
1083                fc.best_fit = c.best_fit;
1084                fc.collapsed = c.collapsed;
1085                fc.custom_width = c.custom_width;
1086                fc.hidden = c.hidden;
1087                fc.phonetic = c.phonetic;
1088                fc.style = c.style;
1089                fc.width = c.width;
1090            },
1091        ),
1092    });
1093}
1094
1095fn set_col_style_ws(ws: &mut XlsxWorksheet, min_val: i32, max_val: i32, style_id: i32) {
1096    let width = ws
1097        .sheet_format_pr
1098        .as_ref()
1099        .and_then(|f| f.default_col_width)
1100        .unwrap_or(DEFAULT_COL_WIDTH);
1101    let col_data = XlsxCol {
1102        min: min_val as i64,
1103        max: max_val as i64,
1104        width: Some(width),
1105        style: Some(style_id as i64),
1106        ..Default::default()
1107    };
1108    ws.cols = Some(XlsxCols {
1109        col: flat_cols(
1110            col_data,
1111            ws.cols.as_ref().map(|c| c.col.as_slice()).unwrap_or(&[]),
1112            |fc, c| {
1113                fc.best_fit = c.best_fit;
1114                fc.collapsed = c.collapsed;
1115                fc.custom_width = c.custom_width;
1116                fc.hidden = c.hidden;
1117                fc.outline_level = c.outline_level;
1118                fc.phonetic = c.phonetic;
1119                fc.width = c.width;
1120            },
1121        ),
1122    });
1123}
1124
1125fn set_col_width_ws(ws: &mut XlsxWorksheet, min_val: i32, max_val: i32, width: f64) {
1126    let col_data = XlsxCol {
1127        min: min_val as i64,
1128        max: max_val as i64,
1129        width: Some(width),
1130        custom_width: Some(true),
1131        ..Default::default()
1132    };
1133    ws.cols = Some(XlsxCols {
1134        col: flat_cols(
1135            col_data,
1136            ws.cols.as_ref().map(|c| c.col.as_slice()).unwrap_or(&[]),
1137            |fc, c| {
1138                fc.best_fit = c.best_fit;
1139                fc.collapsed = c.collapsed;
1140                fc.hidden = c.hidden;
1141                fc.outline_level = c.outline_level;
1142                fc.phonetic = c.phonetic;
1143                fc.style = c.style;
1144            },
1145        ),
1146    });
1147}
1148
1149fn flat_cols<F>(col: XlsxCol, cols: &[XlsxCol], mut replacer: F) -> Vec<XlsxCol>
1150where
1151    F: FnMut(&mut XlsxCol, &XlsxCol),
1152{
1153    let mut fc: HashMap<i64, XlsxCol> = HashMap::new();
1154    for i in col.min..=col.max {
1155        let mut c = col.clone();
1156        c.min = i;
1157        c.max = i;
1158        fc.insert(i, c);
1159    }
1160    for column in cols {
1161        for i in column.min..=column.max {
1162            if let Some(existing) = fc.get_mut(&i) {
1163                replacer(existing, column);
1164            } else {
1165                let mut c = column.clone();
1166                c.min = i;
1167                c.max = i;
1168                fc.insert(i, c);
1169            }
1170        }
1171    }
1172    let mut result: Vec<XlsxCol> = fc.into_values().collect();
1173    result.sort_by_key(|c| c.min);
1174    result
1175}
1176
1177// ------------------------------------------------------------------
1178// Panes and page breaks
1179// ------------------------------------------------------------------
1180
1181fn set_panes_ws(ws: &mut XlsxWorksheet, panes: &Panes) -> Result<()> {
1182    if panes.selection.is_empty() && !panes.freeze && !panes.split {
1183        if let Some(views) = ws.sheet_views.as_mut() {
1184            if let Some(view) = views.sheet_view.last_mut() {
1185                view.pane = None;
1186            }
1187        }
1188        return Ok(());
1189    }
1190
1191    let pane = XlsxPane {
1192        active_pane: Some(panes.active_pane.clone()).filter(|s| !s.is_empty()),
1193        top_left_cell: Some(panes.top_left_cell.clone()).filter(|s| !s.is_empty()),
1194        x_split: Some(panes.x_split as f64),
1195        y_split: Some(panes.y_split as f64),
1196        state: if panes.freeze {
1197            Some("frozen".to_string())
1198        } else {
1199            None
1200        },
1201    };
1202
1203    if ws.sheet_views.is_none() {
1204        ws.sheet_views = Some(XlsxSheetViews::default());
1205    }
1206    let views = ws.sheet_views.as_mut().unwrap();
1207    if views.sheet_view.is_empty() {
1208        views
1209            .sheet_view
1210            .push(crate::xml::worksheet::XlsxSheetView::default());
1211    }
1212    let view = views.sheet_view.last_mut().unwrap();
1213    view.pane = Some(pane);
1214
1215    let selections: Vec<XlsxSelection> = panes
1216        .selection
1217        .iter()
1218        .map(|s| XlsxSelection {
1219            active_cell: Some(s.active_cell.clone()).filter(|s| !s.is_empty()),
1220            active_cell_id: None,
1221            pane: Some(s.pane.clone()).filter(|s| !s.is_empty()),
1222            sqref: Some(s.sqref.clone()).filter(|s| !s.is_empty()),
1223        })
1224        .collect();
1225    view.selection = selections;
1226    Ok(())
1227}
1228
1229fn insert_page_break_ws(ws: &mut XlsxWorksheet, cell: &str) -> Result<()> {
1230    let (mut col, mut row) = cell_name_to_coordinates(cell).map_err(|e| io_err(e))?;
1231    col -= 1;
1232    row -= 1;
1233    if col == 0 && row == 0 {
1234        return Ok(());
1235    }
1236    if ws.row_breaks.is_none() {
1237        ws.row_breaks = Some(XlsxRowBreaks {
1238            breaks: XlsxBreaks::default(),
1239        });
1240    }
1241    if ws.col_breaks.is_none() {
1242        ws.col_breaks = Some(XlsxColBreaks {
1243            breaks: XlsxBreaks::default(),
1244        });
1245    }
1246
1247    let row_breaks = ws.row_breaks.as_mut().unwrap();
1248    let col_breaks = ws.col_breaks.as_mut().unwrap();
1249
1250    let row_exists = row_breaks
1251        .breaks
1252        .brk
1253        .iter()
1254        .any(|b| b.id == Some(row as i64));
1255    let col_exists = col_breaks
1256        .breaks
1257        .brk
1258        .iter()
1259        .any(|b| b.id == Some(col as i64));
1260
1261    if row != 0 && !row_exists {
1262        row_breaks.breaks.brk.push(XlsxBrk {
1263            id: Some(row as i64),
1264            max: Some(MAX_COLUMNS as i64 - 1),
1265            min: None,
1266            man: Some(true),
1267            pt: None,
1268        });
1269        row_breaks.breaks.manual_break_count =
1270            Some(row_breaks.breaks.manual_break_count.unwrap_or(0) + 1);
1271    }
1272    if col != 0 && !col_exists {
1273        col_breaks.breaks.brk.push(XlsxBrk {
1274            id: Some(col as i64),
1275            max: Some(TOTAL_ROWS as i64 - 1),
1276            min: None,
1277            man: Some(true),
1278            pt: None,
1279        });
1280        col_breaks.breaks.manual_break_count =
1281            Some(col_breaks.breaks.manual_break_count.unwrap_or(0) + 1);
1282    }
1283    row_breaks.breaks.count = Some(row_breaks.breaks.brk.len() as i64);
1284    col_breaks.breaks.count = Some(col_breaks.breaks.brk.len() as i64);
1285    Ok(())
1286}
1287
1288// ------------------------------------------------------------------
1289// Table helpers
1290// ------------------------------------------------------------------
1291
1292fn extract_si_text(si: &XlsxSi) -> String {
1293    if let Some(t) = &si.t {
1294        return t.val.clone();
1295    }
1296    si.r.iter()
1297        .filter_map(|r| r.t.as_ref().map(|t| t.val.clone()))
1298        .collect()
1299}
1300
1301fn parse_table_options(opts: &TableOptions) -> Result<TableOptions> {
1302    let mut options = opts.clone();
1303    if options.show_row_stripes.is_none() {
1304        options.show_row_stripes = Some(true);
1305    }
1306    if !options.name.is_empty() {
1307        check_defined_name(&options.name)?;
1308    }
1309    Ok(options)
1310}
1311
1312fn check_defined_name(name: &str) -> Result<()> {
1313    if name.len() > 255 {
1314        return Err(Box::new(crate::errors::ErrNameLength));
1315    }
1316    let mut chars = name.chars();
1317    if let Some(first) = chars.next() {
1318        if !first.is_ascii_alphabetic() && first != '_' {
1319            return Err(crate::errors::new_invalid_name_error(name).into());
1320        }
1321        for c in chars {
1322            if !c.is_ascii_alphanumeric() && c != '_' && c != '.' {
1323                return Err(crate::errors::new_invalid_name_error(name).into());
1324            }
1325        }
1326    }
1327    Ok(())
1328}
1329
1330// ------------------------------------------------------------------
1331// Buffered writer with optional temp-file backing
1332// ------------------------------------------------------------------
1333
1334const TEMPLATE_NAMESPACE_ID_MAP: &str = crate::templates::TEMPLATE_NAMESPACE_ID_MAP;
1335
1336#[derive(Debug)]
1337struct BufferedWriter {
1338    tmp_dir: String,
1339    tmp: Option<FsFile>,
1340    tmp_path: Option<PathBuf>,
1341    buf: Vec<u8>,
1342}
1343
1344impl BufferedWriter {
1345    fn new(tmp_dir: String) -> Self {
1346        Self {
1347            tmp_dir,
1348            tmp: None,
1349            tmp_path: None,
1350            buf: Vec::new(),
1351        }
1352    }
1353
1354    fn write(&mut self, p: &[u8]) -> io::Result<()> {
1355        self.buf.extend_from_slice(p);
1356        Ok(())
1357    }
1358
1359    fn write_str(&mut self, s: &str) -> io::Result<()> {
1360        self.write(s.as_bytes())
1361    }
1362
1363    fn reader(&mut self) -> io::Result<Box<dyn Read + '_>> {
1364        if self.tmp.is_none() {
1365            return Ok(Box::new(Cursor::new(&self.buf)));
1366        }
1367        self.flush()?;
1368        let mut content = Vec::new();
1369        if let Some(path) = &self.tmp_path {
1370            let mut f = FsFile::open(path)?;
1371            f.read_to_end(&mut content)?;
1372        }
1373        Ok(Box::new(Cursor::new(content)))
1374    }
1375
1376    fn sync(&mut self) -> io::Result<()> {
1377        if self.buf.len() < STREAM_CHUNK_SIZE as usize {
1378            return Ok(());
1379        }
1380        if self.tmp.is_none() {
1381            match create_temp(&self.tmp_dir) {
1382                Ok((f, p)) => {
1383                    self.tmp = Some(f);
1384                    self.tmp_path = Some(p);
1385                }
1386                Err(_) => return Ok(()),
1387            }
1388        }
1389        self.flush()
1390    }
1391
1392    fn flush(&mut self) -> io::Result<()> {
1393        if let Some(tmp) = &mut self.tmp {
1394            tmp.write_all(&self.buf)?;
1395            tmp.sync_all()?;
1396            self.buf.clear();
1397        }
1398        Ok(())
1399    }
1400
1401    fn close(&mut self) -> io::Result<()> {
1402        self.buf.clear();
1403        if let Some(tmp) = self.tmp.take() {
1404            drop(tmp);
1405            if let Some(path) = self.tmp_path.take() {
1406                let _ = fs::remove_file(path);
1407            }
1408        }
1409        Ok(())
1410    }
1411
1412    /// Flush any buffered data, close the temporary file handle, and return
1413    /// the path to the backing temporary file. The caller is responsible for
1414    /// deleting the file once it is no longer needed.
1415    fn into_temp_file(&mut self) -> io::Result<Option<PathBuf>> {
1416        self.flush()?;
1417        if let Some(tmp) = self.tmp.take() {
1418            self.buf.clear();
1419            drop(tmp);
1420            return Ok(self.tmp_path.take());
1421        }
1422        if !self.buf.is_empty() {
1423            let (mut f, path) = create_temp(&self.tmp_dir)?;
1424            f.write_all(&self.buf)?;
1425            f.sync_all()?;
1426            drop(f);
1427            self.buf.clear();
1428            return Ok(Some(path));
1429        }
1430        self.buf.clear();
1431        Ok(None)
1432    }
1433}
1434
1435impl Write for BufferedWriter {
1436    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
1437        self.buf.extend_from_slice(buf);
1438        Ok(buf.len())
1439    }
1440
1441    fn flush(&mut self) -> io::Result<()> {
1442        self.flush()
1443    }
1444}
1445
1446impl Drop for BufferedWriter {
1447    fn drop(&mut self) {
1448        let _ = self.close();
1449    }
1450}
1451
1452fn create_temp(tmp_dir: &str) -> io::Result<(FsFile, PathBuf)> {
1453    const MAX_RETRIES: usize = 100;
1454
1455    let dir = if tmp_dir.is_empty() {
1456        std::env::temp_dir()
1457    } else {
1458        PathBuf::from(tmp_dir)
1459    };
1460    let now = SystemTime::now()
1461        .duration_since(UNIX_EPOCH)
1462        .unwrap_or_default();
1463    let thread_id = format!("{:?}", thread::current().id())
1464        .chars()
1465        .map(|c| if c.is_alphanumeric() { c } else { '_' })
1466        .collect::<String>();
1467
1468    for _ in 0..MAX_RETRIES {
1469        let path = dir.join(format!(
1470            "excelize-{}-{}-{}-{}.xml",
1471            now.as_secs(),
1472            now.subsec_nanos(),
1473            thread_id,
1474            rand::random::<u32>()
1475        ));
1476        match fs::OpenOptions::new()
1477            .write(true)
1478            .create_new(true)
1479            .open(&path)
1480        {
1481            Ok(file) => return Ok((file, path)),
1482            Err(e) if e.kind() == io::ErrorKind::AlreadyExists => continue,
1483            Err(e) => return Err(e),
1484        }
1485    }
1486    Err(io::Error::new(
1487        io::ErrorKind::Other,
1488        "failed to create a unique temporary file after 100 attempts",
1489    ))
1490}
1491
1492// ------------------------------------------------------------------
1493// XML / string helpers
1494// ------------------------------------------------------------------
1495
1496fn escape_xml(s: &str) -> String {
1497    escape(s).into_owned()
1498}
1499
1500fn io_err(s: String) -> Box<dyn std::error::Error + Send + Sync> {
1501    Box::new(io::Error::new(io::ErrorKind::InvalidData, s))
1502}
1503
1504fn needs_space_preserve(s: &str) -> bool {
1505    if s.is_empty() {
1506        return false;
1507    }
1508    let prefix = s.as_bytes().first().copied().unwrap_or(0);
1509    let suffix = s.as_bytes().last().copied().unwrap_or(0);
1510    [b' ', b'\t', b'\n', b'\r'].contains(&prefix) || [b' ', b'\t', b'\n', b'\r'].contains(&suffix)
1511}
1512
1513fn format_float(value: f64) -> String {
1514    let s = format!("{value:.15}");
1515    if s.contains('.') {
1516        s.trim_end_matches('0').trim_end_matches('.').to_string()
1517    } else {
1518        s
1519    }
1520}
1521
1522#[cfg(test)]
1523mod tests {
1524    use super::*;
1525    use crate::options::Options;
1526    use chrono::{NaiveDate, NaiveTime};
1527
1528    #[test]
1529    fn stream_writer_basic_round_trip() {
1530        let mut f = File::new_with_options(Options::default());
1531        let mut sw = f.new_stream_writer("Sheet1").unwrap();
1532
1533        sw.set_row(
1534            "A1",
1535            &[
1536                &CellValue::String("Data".to_string()),
1537                &CellValue::Int(42),
1538                &CellValue::Float(3.14),
1539                &CellValue::Bool(true),
1540            ],
1541            None,
1542        )
1543        .unwrap();
1544        sw.set_row(
1545            "A2",
1546            &[&CellValue::Int(1), &CellValue::Int(2)],
1547            Some(RowOpts::default()),
1548        )
1549        .unwrap();
1550        sw.flush().unwrap();
1551        drop(sw);
1552
1553        let tmp = std::env::temp_dir().join("excelize_stream_basic_test.xlsx");
1554        f.save_as(tmp.to_str().unwrap()).unwrap();
1555
1556        let f2 = File::open_file(tmp.to_str().unwrap(), Options::default()).unwrap();
1557        assert_eq!(f2.get_cell_value("Sheet1", "A1").unwrap(), "Data");
1558        assert_eq!(f2.get_cell_value("Sheet1", "B1").unwrap(), "42");
1559        assert_eq!(f2.get_cell_value("Sheet1", "C1").unwrap(), "3.14");
1560        assert_eq!(f2.get_cell_value("Sheet1", "D1").unwrap(), "TRUE");
1561        assert_eq!(f2.get_cell_value("Sheet1", "A2").unwrap(), "1");
1562        let _ = std::fs::remove_file(&tmp);
1563    }
1564
1565    #[test]
1566    fn stream_writer_cell_with_style_and_formula() {
1567        let mut f = File::new_with_options(Options::default());
1568        let style_id = f
1569            .new_style(&Style {
1570                font: Some(crate::styles::Font {
1571                    color: Some("777777".to_string()),
1572                    ..Default::default()
1573                }),
1574                ..Default::default()
1575            })
1576            .unwrap();
1577
1578        let mut sw = f.new_stream_writer("Sheet1").unwrap();
1579        sw.set_row(
1580            "A1",
1581            &[
1582                &Cell {
1583                    style_id,
1584                    value: Some(CellValue::String("Styled".to_string())),
1585                    ..Default::default()
1586                },
1587                &Cell {
1588                    formula: "SUM(A1,A1)".to_string(),
1589                    value: Some(CellValue::String("formula value".to_string())),
1590                    ..Default::default()
1591                },
1592            ],
1593            None,
1594        )
1595        .unwrap();
1596        sw.flush().unwrap();
1597        drop(sw);
1598
1599        let tmp = std::env::temp_dir().join("excelize_stream_formula_test.xlsx");
1600        f.save_as(tmp.to_str().unwrap()).unwrap();
1601        let f2 = File::open_file(tmp.to_str().unwrap(), Options::default()).unwrap();
1602        assert_eq!(f2.get_cell_value("Sheet1", "A1").unwrap(), "Styled");
1603        assert_eq!(f2.get_cell_value("Sheet1", "B1").unwrap(), "formula value");
1604        let _ = std::fs::remove_file(&tmp);
1605    }
1606
1607    #[test]
1608    fn stream_writer_row_and_col_options() {
1609        let mut f = File::new_with_options(Options::default());
1610        let mut sw = f.new_stream_writer("Sheet1").unwrap();
1611        sw.set_col_width(1, 3, 20.0).unwrap();
1612        sw.set_col_visible(2, 2, false).unwrap();
1613        sw.set_row(
1614            "A1",
1615            &[&CellValue::Int(1)],
1616            Some(RowOpts {
1617                height: 30.0,
1618                hidden: false,
1619                style_id: 0,
1620                outline_level: 2,
1621            }),
1622        )
1623        .unwrap();
1624        sw.flush().unwrap();
1625        drop(sw);
1626
1627        let tmp = std::env::temp_dir().join("excelize_stream_options_test.xlsx");
1628        f.save_as(tmp.to_str().unwrap()).unwrap();
1629        let f2 = File::open_file(tmp.to_str().unwrap(), Options::default()).unwrap();
1630        assert_eq!(f2.get_row_height("Sheet1", 1).unwrap(), 30.0);
1631        assert!(!f2.get_col_visible("Sheet1", "B").unwrap());
1632        assert_eq!(f2.get_col_width("Sheet1", "A").unwrap(), 20.0);
1633        let _ = std::fs::remove_file(&tmp);
1634    }
1635
1636    #[test]
1637    fn stream_writer_merge_cell() {
1638        let mut f = File::new_with_options(Options::default());
1639        let mut sw = f.new_stream_writer("Sheet1").unwrap();
1640        sw.set_row("A1", &[&CellValue::Int(1)], None).unwrap();
1641        sw.merge_cell("A1", "B2").unwrap();
1642        sw.flush().unwrap();
1643        drop(sw);
1644
1645        let tmp = std::env::temp_dir().join("excelize_stream_merge_test.xlsx");
1646        f.save_as(tmp.to_str().unwrap()).unwrap();
1647        let f2 = File::open_file(tmp.to_str().unwrap(), Options::default()).unwrap();
1648        let ws = f2.work_sheet_reader("Sheet1").unwrap();
1649        assert!(ws.merge_cells.is_some());
1650        let _ = std::fs::remove_file(&tmp);
1651    }
1652
1653    #[test]
1654    fn stream_writer_date_and_time() {
1655        let mut f = File::new_with_options(Options::default());
1656        let mut sw = f.new_stream_writer("Sheet1").unwrap();
1657        let dt = NaiveDate::from_ymd_opt(2024, 7, 13)
1658            .unwrap()
1659            .and_hms_opt(12, 30, 0)
1660            .unwrap();
1661        sw.set_row(
1662            "A1",
1663            &[
1664                &CellValue::DateTime(dt),
1665                &CellValue::Time(NaiveTime::from_hms_opt(14, 30, 0).unwrap()),
1666            ],
1667            None,
1668        )
1669        .unwrap();
1670        sw.flush().unwrap();
1671        drop(sw);
1672
1673        let tmp = std::env::temp_dir().join("excelize_stream_date_test.xlsx");
1674        f.save_as(tmp.to_str().unwrap()).unwrap();
1675        let f2 = File::open_file(tmp.to_str().unwrap(), Options::default()).unwrap();
1676        assert_eq!(f2.get_cell_value("Sheet1", "A1").unwrap(), "7/13/24 12:30");
1677        assert_eq!(f2.get_cell_value("Sheet1", "B1").unwrap(), "14:30:00");
1678        let _ = std::fs::remove_file(&tmp);
1679    }
1680
1681    #[test]
1682    fn stream_writer_row_order_error() {
1683        let f = File::new_with_options(Options::default());
1684        let mut sw = f.new_stream_writer("Sheet1").unwrap();
1685        sw.set_row("A2", &[&CellValue::Int(1)], None).unwrap();
1686        assert!(sw.set_row("A1", &[&CellValue::Int(1)], None).is_err());
1687    }
1688
1689    #[test]
1690    fn stream_writer_invalid_column_errors() {
1691        let f = File::new_with_options(Options::default());
1692        let mut sw = f.new_stream_writer("Sheet1").unwrap();
1693        assert!(sw.set_col_width(0, 1, 10.0).is_err());
1694        assert!(sw.set_col_width(1, 1, 300.0).is_err());
1695        assert!(sw.set_col_outline_level(1, 8).is_err());
1696    }
1697
1698    #[test]
1699    fn stream_writer_mixed_types_and_nil_values() {
1700        let mut f = File::new_with_options(Options::default());
1701        let style_id = f
1702            .new_style(&Style {
1703                font: Some(crate::styles::Font {
1704                    color: Some("777777".to_string()),
1705                    ..Default::default()
1706                }),
1707                ..Default::default()
1708            })
1709            .unwrap();
1710
1711        let mut sw = f.new_stream_writer("Sheet1").unwrap();
1712        sw.set_row(
1713            "A1",
1714            &[
1715                &1i32 as &dyn StreamCellValue,
1716                &2.5f64,
1717                &"hello",
1718                &true,
1719                &None::<CellValue>,
1720                &Cell {
1721                    style_id,
1722                    value: None,
1723                    ..Default::default()
1724                },
1725                &Cell {
1726                    value: Some(CellValue::String("with value".to_string())),
1727                    ..Default::default()
1728                },
1729            ],
1730            None,
1731        )
1732        .unwrap();
1733        sw.flush().unwrap();
1734        drop(sw);
1735
1736        let tmp = std::env::temp_dir().join("excelize_stream_mixed_test.xlsx");
1737        f.save_as(tmp.to_str().unwrap()).unwrap();
1738        let f2 = File::open_file(tmp.to_str().unwrap(), Options::default()).unwrap();
1739        assert_eq!(f2.get_cell_value("Sheet1", "A1").unwrap(), "1");
1740        assert_eq!(f2.get_cell_value("Sheet1", "B1").unwrap(), "2.5");
1741        assert_eq!(f2.get_cell_value("Sheet1", "C1").unwrap(), "hello");
1742        assert_eq!(f2.get_cell_value("Sheet1", "D1").unwrap(), "TRUE");
1743        assert_eq!(f2.get_cell_value("Sheet1", "E1").unwrap(), "");
1744        assert_eq!(f2.get_cell_value("Sheet1", "F1").unwrap(), "");
1745        assert_eq!(f2.get_cell_value("Sheet1", "G1").unwrap(), "with value");
1746        assert_eq!(f2.get_cell_style("Sheet1", "F1").unwrap(), style_id);
1747        let _ = std::fs::remove_file(&tmp);
1748    }
1749
1750    #[test]
1751    fn stream_writer_empty_string_is_written() {
1752        let mut f = File::new_with_options(Options::default());
1753        let mut sw = f.new_stream_writer("Sheet1").unwrap();
1754        sw.set_row(
1755            "A1",
1756            &[&CellValue::String("".to_string()) as &dyn StreamCellValue],
1757            None,
1758        )
1759        .unwrap();
1760        sw.flush().unwrap();
1761        drop(sw);
1762
1763        let tmp = std::env::temp_dir().join("excelize_stream_empty_str_test.xlsx");
1764        f.save_as(tmp.to_str().unwrap()).unwrap();
1765        let f2 = File::open_file(tmp.to_str().unwrap(), Options::default()).unwrap();
1766        assert_eq!(f2.get_cell_value("Sheet1", "A1").unwrap(), "");
1767        let _ = std::fs::remove_file(&tmp);
1768    }
1769}