Skip to main content

excelize_rs/
sheet.rs

1//! Worksheet-level API.
2//!
3//! This module corresponds to `sheet.go`, `sheetpr.go` and `sheetview.go` in
4//! the Go implementation.
5
6use std::collections::HashMap;
7use std::io::Cursor;
8use std::path::Path;
9
10use quick_xml::Reader;
11use quick_xml::events::Event;
12
13use crate::constants::{
14    CONTENT_TYPE_SPREADSHEET_ML_WORKSHEET, MAX_COLUMNS, MAX_FIELD_LENGTH, SOURCE_RELATIONSHIP,
15    SOURCE_RELATIONSHIP_IMAGE, SOURCE_RELATIONSHIP_SHARED_STRINGS, SOURCE_RELATIONSHIP_WORKSHEET,
16    TOTAL_ROWS,
17};
18use crate::crypt::gen_iso_passwd_hash;
19use crate::errors::Result;
20use crate::errors::{
21    ErrGroupSheets, ErrImgExt, ErrParameterInvalid, ErrSheetIdx, ErrSheetNotExist,
22    ErrUnprotectSheet, ErrUnprotectSheetPassword, ErrWorkbook, new_field_length_error,
23};
24use crate::file::{File, namespace_strict_to_transitional};
25use crate::lib_util::{
26    cell_name_to_coordinates, coordinates_to_range_ref, count_utf16_string, in_str_slice,
27    range_ref_to_coordinates, sort_coordinates,
28};
29use crate::xml::content_types::{XlsxContentTypeEntry, XlsxDefault, XlsxOverride};
30use crate::xml::workbook::{XlsxBookViews, XlsxSheet, XlsxWorkBookView};
31pub use crate::xml::worksheet::{Panes, Selection, SheetProtectionOptions};
32use crate::xml::worksheet::{
33    XlsxBreaks, XlsxBrk, XlsxColBreaks, XlsxDimension, XlsxHeaderFooter, XlsxIgnoredError,
34    XlsxIgnoredErrors, XlsxPageMargins, XlsxPageSetUp, XlsxPane, XlsxPicture, XlsxPrintOptions,
35    XlsxRowBreaks, XlsxSelection, XlsxSheetFormatPr, XlsxSheetPr, XlsxSheetProtection,
36    XlsxSheetView, XlsxSheetViews, XlsxWorksheet,
37};
38
39/// Type of error to ignore for a range of cells.
40#[derive(Debug, Clone, Copy, PartialEq, Eq)]
41pub enum IgnoredErrorsType {
42    EvalError = 0,
43    TwoDigitTextYear = 1,
44    NumberStoredAsText = 2,
45    Formula = 3,
46    FormulaRange = 4,
47    UnlockedFormula = 5,
48    EmptyCellReference = 6,
49    ListDataValidation = 7,
50    CalculatedColumn = 8,
51}
52
53/// Options for `SetSheetProps`.
54#[derive(Debug, Default, Clone, PartialEq)]
55pub struct SheetPropsOptions {
56    pub code_name: Option<String>,
57    pub enable_format_conditions_calculation: Option<bool>,
58    pub published: Option<bool>,
59    pub auto_page_breaks: Option<bool>,
60    pub fit_to_page: Option<bool>,
61    pub tab_color_indexed: Option<i64>,
62    pub tab_color_rgb: Option<String>,
63    pub tab_color_theme: Option<i64>,
64    pub tab_color_tint: Option<f64>,
65    pub outline_pr_apply_styles: Option<bool>,
66    pub outline_pr_summary_below: Option<bool>,
67    pub outline_pr_summary_right: Option<bool>,
68    pub outline_pr_show_outline_symbols: Option<bool>,
69    pub base_col_width: Option<u8>,
70    pub default_col_width: Option<f64>,
71    pub default_row_height: Option<f64>,
72    pub custom_height: Option<bool>,
73    pub zero_height: Option<bool>,
74    pub thick_top: Option<bool>,
75    pub thick_bottom: Option<bool>,
76}
77
78/// Options for `SetSheetView`.
79#[derive(Debug, Default, Clone, PartialEq)]
80pub struct SheetViewOptions {
81    pub default_grid_color: Option<bool>,
82    pub right_to_left: Option<bool>,
83    pub show_formulas: Option<bool>,
84    pub show_grid_lines: Option<bool>,
85    pub show_outline_symbols: Option<bool>,
86    pub show_row_col_headers: Option<bool>,
87    pub show_ruler: Option<bool>,
88    pub show_white_space: Option<bool>,
89    pub show_zeros: Option<bool>,
90    pub tab_selected: Option<bool>,
91    pub top_left_cell: Option<String>,
92    pub view: Option<String>,
93    pub window_protection: Option<bool>,
94    pub zoom_scale: Option<f64>,
95}
96
97/// Options for `SetPageLayout`.
98#[derive(Debug, Default, Clone, PartialEq)]
99pub struct PageLayoutOptions {
100    pub black_and_white: Option<bool>,
101    pub cell_comments: Option<String>,
102    pub copies: Option<i64>,
103    pub draft: Option<bool>,
104    pub errors: Option<String>,
105    pub first_page_number: Option<String>,
106    pub fit_to_height: Option<i64>,
107    pub fit_to_width: Option<i64>,
108    pub horizontal_dpi: Option<String>,
109    pub orientation: Option<String>,
110    pub page_order: Option<String>,
111    pub paper_size: Option<i64>,
112    pub scale: Option<i64>,
113    pub use_first_page_number: Option<bool>,
114    pub use_printer_defaults: Option<bool>,
115    pub vertical_dpi: Option<String>,
116}
117
118/// Options for `SetPageMargins`.
119#[derive(Debug, Default, Clone, PartialEq)]
120pub struct PageMarginsOptions {
121    pub left: Option<f64>,
122    pub right: Option<f64>,
123    pub top: Option<f64>,
124    pub bottom: Option<f64>,
125    pub header: Option<f64>,
126    pub footer: Option<f64>,
127    pub horizontally: Option<bool>,
128    pub vertically: Option<bool>,
129}
130
131/// Header/footer options for `SetHeaderFooter`.
132#[derive(Debug, Default, Clone, PartialEq)]
133pub struct HeaderFooterOptions {
134    pub different_first: Option<bool>,
135    pub different_odd_even: Option<bool>,
136    pub scale_with_doc: Option<bool>,
137    pub align_with_margins: Option<bool>,
138    pub odd_header: Option<String>,
139    pub odd_footer: Option<String>,
140    pub even_header: Option<String>,
141    pub even_footer: Option<String>,
142    pub first_header: Option<String>,
143    pub first_footer: Option<String>,
144}
145
146// ------------------------------------------------------------------
147// Public API
148// ------------------------------------------------------------------
149
150impl File {
151    /// Get a list of worksheet / chart / dialog sheet names.
152    pub fn get_sheet_list(&self) -> Vec<String> {
153        let wb = self.workbook_reader().ok();
154        let mut list = Vec::new();
155        if let Some(wb) = wb {
156            for sheet in &wb.sheets.sheet {
157                if let Some(name) = &sheet.name {
158                    list.push(name.clone());
159                }
160            }
161        }
162        list
163    }
164
165    /// Create a new worksheet.
166    pub fn new_sheet(&self, name: &str) -> Result<i32> {
167        crate::excelize::check_sheet_name(name)?;
168        if let Ok(index) = self.get_sheet_index(name) {
169            return Ok(index);
170        }
171
172        let mut wb = self.workbook_reader()?;
173        let mut rels = self
174            .rels_reader(&self.get_workbook_rels_path())?
175            .unwrap_or_default();
176
177        // Find next sheet id and relationship id.
178        let mut max_sheet_id = 0i64;
179        let mut max_rid = 0i32;
180        for sheet in &wb.sheets.sheet {
181            if let Some(id) = sheet.sheet_id {
182                if id > max_sheet_id {
183                    max_sheet_id = id;
184                }
185            }
186            if let Some(rid) = &sheet.id {
187                if let Ok(n) = rid.trim_start_matches("rId").parse::<i32>() {
188                    if n > max_rid {
189                        max_rid = n;
190                    }
191                }
192            }
193        }
194        for rel in &rels.relationships {
195            if let Ok(n) = rel.id.trim_start_matches("rId").parse::<i32>() {
196                if n > max_rid {
197                    max_rid = n;
198                }
199            }
200        }
201        max_sheet_id += 1;
202        max_rid += 1;
203
204        let new_rid = format!("rId{max_rid}");
205        let sheet_xml = format!("xl/worksheets/sheet{max_sheet_id}.xml");
206
207        wb.sheets.sheet.push(XlsxSheet {
208            name: Some(name.to_string()),
209            sheet_id: Some(max_sheet_id),
210            id: Some(new_rid.clone()),
211            plain_id: None,
212            state: None,
213        });
214
215        rels.relationships
216            .push(crate::xml::workbook::XlsxRelationship {
217                id: new_rid,
218                r#type: SOURCE_RELATIONSHIP_WORKSHEET.to_string(),
219                target: format!("worksheets/sheet{max_sheet_id}.xml"),
220                target_mode: None,
221            });
222
223        // Insert worksheet part.
224        let ws = new_worksheet();
225        self.sheet.insert(sheet_xml.clone(), ws.clone());
226        self.pkg.insert(sheet_xml.clone(), worksheet_bytes(&ws));
227        // Register the standard worksheet namespaces so the part is written
228        // back with the same root attributes as the template sheet.
229        if let Some(attrs) =
230            crate::file::extract_root_namespace_attributes(crate::templates::TEMPLATE_SHEET.as_bytes())
231        {
232            self.xml_attr.insert(sheet_xml.clone(), attrs);
233        }
234        self.checked.insert(sheet_xml.clone(), true);
235
236        // Update content types.
237        let mut ct = self.content_types_reader()?;
238        ct.entries
239            .push(XlsxContentTypeEntry::Override(XlsxOverride {
240                part_name: format!("/{sheet_xml}"),
241                content_type: CONTENT_TYPE_SPREADSHEET_ML_WORKSHEET.to_string(),
242            }));
243
244        // Ensure shared strings relationship exists.
245        ensure_shared_strings_rel(&mut rels);
246
247        *self.workbook.lock().unwrap() = Some(wb);
248        self.relationships
249            .insert(self.get_workbook_rels_path(), rels);
250        *self.content_types.lock().unwrap() = Some(ct);
251        self.sheet_map
252            .lock()
253            .unwrap()
254            .insert(name.to_string(), sheet_xml);
255
256        let mut count = self.sheet_count.lock().unwrap();
257        *count += 1;
258        Ok(max_sheet_id as i32)
259    }
260
261    /// Get the worksheet name from a 1-based sheet index.
262    pub fn get_sheet_name(&self, index: i32) -> Result<String> {
263        let wb = self.workbook_reader()?;
264        if index < 1 || index as usize > wb.sheets.sheet.len() {
265            return Err(Box::new(ErrSheetIdx));
266        }
267        Ok(wb.sheets.sheet[index as usize - 1]
268            .name
269            .clone()
270            .unwrap_or_default())
271    }
272
273    /// Get the 1-based sheet index of a worksheet by name.
274    pub fn get_sheet_index(&self, name: &str) -> Result<i32> {
275        let wb = self.workbook_reader()?;
276        for (i, sheet) in wb.sheets.sheet.iter().enumerate() {
277            if sheet
278                .name
279                .as_deref()
280                .map(|n| n.eq_ignore_ascii_case(name))
281                .unwrap_or(false)
282            {
283                return Ok(i as i32 + 1);
284            }
285        }
286        Err(Box::new(ErrSheetNotExist {
287            sheet_name: name.to_string(),
288        }))
289    }
290
291    /// Delete a worksheet by name.
292    pub fn delete_sheet(&self, name: &str) -> Result<()> {
293        crate::excelize::check_sheet_name(name)?;
294        let index = self.get_sheet_index(name)?;
295        if self.get_sheet_list().len() <= 1 {
296            return Err(Box::new(ErrWorkbook));
297        }
298        let path = self
299            .get_sheet_xml_path(name)
300            .ok_or_else(|| ErrSheetNotExist {
301                sheet_name: name.to_string(),
302            })?;
303
304        self.clear_calc_cache();
305        let active_index = self.get_active_sheet_index()?;
306        let sheet_id = self.get_sheet_id(name);
307
308        let mut wb = self.workbook_reader()?;
309        wb.sheets.sheet.remove(index as usize - 1);
310
311        let rel_path = self.get_workbook_rels_path();
312        let mut rels = self.rels_reader(&rel_path)?.unwrap_or_default();
313        rels.relationships.retain(|r| {
314            let target = r.target.to_lowercase();
315            !target.contains(&format!("/{path}").to_lowercase()) && target != path.to_lowercase()
316        });
317
318        let sheet_rels = format!(
319            "xl/worksheets/_rels/{}.rels",
320            path.trim_start_matches("xl/worksheets/")
321        );
322
323        *self.workbook.lock().unwrap() = Some(wb);
324        self.relationships.insert(rel_path, rels);
325        self.sheet.remove(&path);
326        self.pkg.remove(&path);
327        self.checked.remove(&path);
328        self.relationships.remove(&sheet_rels);
329        self.pkg.remove(&sheet_rels);
330        self.sheet_map.lock().unwrap().remove(name);
331
332        self.remove_content_types_part(CONTENT_TYPE_SPREADSHEET_ML_WORKSHEET, &format!("/{path}"))?;
333        if sheet_id > 0 {
334            let _ = self.delete_calc_chain(sheet_id, "");
335        }
336
337        let mut new_active = active_index;
338        if active_index == index {
339            new_active = 1;
340        } else if active_index > index {
341            new_active = active_index - 1;
342        }
343        self.set_active_sheet(new_active)?;
344
345        let mut count = self.sheet_count.lock().unwrap();
346        *count -= 1;
347        Ok(())
348    }
349
350    /// Get the index of the active worksheet (1-based).
351    pub fn get_active_sheet_index(&self) -> Result<i32> {
352        let wb = self.workbook_reader()?;
353        let views = wb.book_views.as_ref().and_then(|v| v.workbook_view.first());
354        if let Some(view) = views {
355            return Ok(view.active_tab.unwrap_or(0) as i32 + 1);
356        }
357        Ok(1)
358    }
359
360    /// Set the active worksheet by 1-based index.
361    pub fn set_active_sheet(&self, index: i32) -> Result<()> {
362        let mut wb = self.workbook_reader()?;
363        if index < 1 || index as usize > wb.sheets.sheet.len() {
364            return Err(Box::new(ErrSheetIdx));
365        }
366        if wb.book_views.is_none() {
367            wb.book_views = Some(XlsxBookViews::default());
368        }
369        let views = wb.book_views.as_mut().unwrap();
370        if views.workbook_view.is_empty() {
371            views.workbook_view.push(XlsxWorkBookView::default());
372        }
373        views.workbook_view[0].active_tab = Some(index as i64 - 1);
374        *self.workbook.lock().unwrap() = Some(wb);
375
376        let target = index as usize;
377        for (idx, name) in self.get_sheet_list().iter().enumerate() {
378            let Ok(mut ws) = self.work_sheet_reader(name) else {
379                continue;
380            };
381            if ws.sheet_views.is_none() {
382                ws.sheet_views = Some(XlsxSheetViews {
383                    sheet_view: vec![XlsxSheetView::default()],
384                });
385            }
386            let sv = ws.sheet_views.as_mut().unwrap();
387            if sv.sheet_view.is_empty() {
388                sv.sheet_view.push(XlsxSheetView::default());
389            }
390            sv.sheet_view[0].tab_selected = Some(idx + 1 == target);
391            if let Some(path) = self.get_sheet_xml_path(name) {
392                self.sheet.insert(path, ws);
393            }
394        }
395        Ok(())
396    }
397
398    /// Set worksheet properties.
399    pub fn set_sheet_props(&self, sheet: &str, opts: &SheetPropsOptions) -> Result<()> {
400        let path = self
401            .get_sheet_xml_path(sheet)
402            .ok_or_else(|| ErrSheetNotExist {
403                sheet_name: sheet.to_string(),
404            })?;
405        let mut ws = self.work_sheet_reader(sheet)?;
406        if ws.sheet_pr.is_none() {
407            ws.sheet_pr = Some(XlsxSheetPr::default());
408        }
409        let pr = ws.sheet_pr.as_mut().unwrap();
410        if opts.code_name.is_some() {
411            pr.code_name = opts.code_name.clone();
412        }
413        if opts.enable_format_conditions_calculation.is_some() {
414            pr.enable_format_conditions_calculation = opts.enable_format_conditions_calculation;
415        }
416        if opts.published.is_some() {
417            pr.published = opts.published;
418        }
419        if let Some(indexed) = opts.tab_color_indexed {
420            if pr.tab_color.is_none() {
421                pr.tab_color = Some(crate::xml::common::XlsxColor::default());
422            }
423            pr.tab_color.as_mut().unwrap().indexed = Some(indexed);
424        }
425        if let Some(ref rgb) = opts.tab_color_rgb {
426            if pr.tab_color.is_none() {
427                pr.tab_color = Some(crate::xml::common::XlsxColor::default());
428            }
429            pr.tab_color.as_mut().unwrap().rgb = Some(rgb.clone());
430        }
431        if let Some(theme) = opts.tab_color_theme {
432            if pr.tab_color.is_none() {
433                pr.tab_color = Some(crate::xml::common::XlsxColor::default());
434            }
435            pr.tab_color.as_mut().unwrap().theme = Some(theme);
436        }
437        if let Some(tint) = opts.tab_color_tint {
438            if pr.tab_color.is_none() {
439                pr.tab_color = Some(crate::xml::common::XlsxColor::default());
440            }
441            pr.tab_color.as_mut().unwrap().tint = Some(tint);
442        }
443        let outline_needed = opts.outline_pr_apply_styles.is_some()
444            || opts.outline_pr_summary_below.is_some()
445            || opts.outline_pr_summary_right.is_some()
446            || opts.outline_pr_show_outline_symbols.is_some();
447        if outline_needed {
448            if pr.outline_pr.is_none() {
449                pr.outline_pr = Some(crate::xml::worksheet::XlsxOutlinePr::default());
450            }
451            let outline = pr.outline_pr.as_mut().unwrap();
452            if opts.outline_pr_apply_styles.is_some() {
453                outline.apply_styles = opts.outline_pr_apply_styles;
454            }
455            if opts.outline_pr_summary_below.is_some() {
456                outline.summary_below = opts.outline_pr_summary_below;
457            }
458            if opts.outline_pr_summary_right.is_some() {
459                outline.summary_right = opts.outline_pr_summary_right;
460            }
461            if opts.outline_pr_show_outline_symbols.is_some() {
462                outline.show_outline_symbols = opts.outline_pr_show_outline_symbols;
463            }
464        }
465        if opts.auto_page_breaks.is_some() || opts.fit_to_page.is_some() {
466            if pr.page_set_up_pr.is_none() {
467                pr.page_set_up_pr = Some(crate::xml::worksheet::XlsxPageSetUpPr::default());
468            }
469            let pup = pr.page_set_up_pr.as_mut().unwrap();
470            if opts.auto_page_breaks.is_some() {
471                pup.auto_page_breaks = opts.auto_page_breaks;
472            }
473            if opts.fit_to_page.is_some() {
474                pup.fit_to_page = opts.fit_to_page;
475            }
476        }
477        if ws.sheet_format_pr.is_none() {
478            ws.sheet_format_pr = Some(XlsxSheetFormatPr {
479                default_row_height: 15.0,
480                ..Default::default()
481            });
482        }
483        let sfp = ws.sheet_format_pr.as_mut().unwrap();
484        if let Some(v) = opts.base_col_width {
485            sfp.base_col_width = Some(v);
486        }
487        if let Some(v) = opts.default_col_width {
488            sfp.default_col_width = Some(v);
489        }
490        if let Some(v) = opts.default_row_height {
491            sfp.default_row_height = v;
492        }
493        if let Some(v) = opts.custom_height {
494            sfp.custom_height = Some(v);
495        }
496        if let Some(v) = opts.zero_height {
497            sfp.zero_height = Some(v);
498        }
499        if let Some(v) = opts.thick_top {
500            sfp.thick_top = Some(v);
501        }
502        if let Some(v) = opts.thick_bottom {
503            sfp.thick_bottom = Some(v);
504        }
505        self.sheet.insert(path, ws);
506        Ok(())
507    }
508
509    /// Get worksheet properties.
510    pub fn get_sheet_props(&self, sheet: &str) -> Result<SheetPropsOptions> {
511        let ws = self.work_sheet_reader(sheet)?;
512        let mut opts = SheetPropsOptions {
513            enable_format_conditions_calculation: Some(true),
514            published: Some(true),
515            auto_page_breaks: Some(true),
516            outline_pr_summary_below: Some(true),
517            base_col_width: Some(8),
518            ..Default::default()
519        };
520        if let Some(pr) = ws.sheet_pr {
521            opts.code_name = pr.code_name;
522            if pr.enable_format_conditions_calculation.is_some() {
523                opts.enable_format_conditions_calculation = pr.enable_format_conditions_calculation;
524            }
525            if pr.published.is_some() {
526                opts.published = pr.published;
527            }
528            if let Some(tc) = pr.tab_color {
529                opts.tab_color_indexed = tc.indexed;
530                opts.tab_color_rgb = tc.rgb;
531                opts.tab_color_theme = tc.theme;
532                opts.tab_color_tint = tc.tint;
533            }
534            if let Some(o) = pr.outline_pr {
535                opts.outline_pr_apply_styles = o.apply_styles;
536                opts.outline_pr_summary_below = o.summary_below;
537                opts.outline_pr_summary_right = o.summary_right;
538                opts.outline_pr_show_outline_symbols = o.show_outline_symbols;
539            }
540            if let Some(pup) = pr.page_set_up_pr {
541                opts.auto_page_breaks = pup.auto_page_breaks;
542                opts.fit_to_page = pup.fit_to_page;
543            }
544        }
545        if let Some(sfp) = ws.sheet_format_pr {
546            opts.base_col_width = sfp.base_col_width.or(Some(8));
547            opts.default_col_width = sfp.default_col_width;
548            opts.default_row_height = Some(sfp.default_row_height);
549            opts.custom_height = sfp.custom_height;
550            opts.zero_height = sfp.zero_height;
551            opts.thick_top = sfp.thick_top;
552            opts.thick_bottom = sfp.thick_bottom;
553        }
554        Ok(opts)
555    }
556
557    /// Set worksheet view properties on the given sheet view. The view index may
558    /// be negative and counts backward from the last view.
559    pub fn set_sheet_view(
560        &self,
561        sheet: &str,
562        view_index: i32,
563        opts: &SheetViewOptions,
564    ) -> Result<()> {
565        let path = self
566            .get_sheet_xml_path(sheet)
567            .ok_or_else(|| ErrSheetNotExist {
568                sheet_name: sheet.to_string(),
569            })?;
570        let mut ws = self.work_sheet_reader(sheet)?;
571        if ws.sheet_views.is_none() {
572            ws.sheet_views = Some(XlsxSheetViews {
573                sheet_view: vec![XlsxSheetView::default()],
574            });
575        }
576        let views = ws.sheet_views.as_mut().unwrap();
577        let len = views.sheet_view.len();
578        let idx = if view_index < 0 {
579            if view_index < -(len as i32) {
580                return Err(crate::errors::new_view_idx_error(view_index).into());
581            }
582            (len as i32 + view_index) as usize
583        } else if view_index >= len as i32 {
584            return Err(crate::errors::new_view_idx_error(view_index).into());
585        } else {
586            view_index as usize
587        };
588        let view = &mut views.sheet_view[idx];
589        if opts.default_grid_color.is_some() {
590            view.default_grid_color = opts.default_grid_color;
591        }
592        if opts.right_to_left.is_some() {
593            view.right_to_left = opts.right_to_left;
594        }
595        if opts.show_formulas.is_some() {
596            view.show_formulas = opts.show_formulas;
597        }
598        if opts.show_grid_lines.is_some() {
599            view.show_grid_lines = opts.show_grid_lines;
600        }
601        if opts.show_outline_symbols.is_some() {
602            view.show_outline_symbols = opts.show_outline_symbols;
603        }
604        if opts.show_row_col_headers.is_some() {
605            view.show_row_col_headers = opts.show_row_col_headers;
606        }
607        if opts.show_ruler.is_some() {
608            view.show_ruler = opts.show_ruler;
609        }
610        if opts.show_white_space.is_some() {
611            view.show_white_space = opts.show_white_space;
612        }
613        if opts.show_zeros.is_some() {
614            view.show_zeros = opts.show_zeros;
615        }
616        if opts.tab_selected.is_some() {
617            view.tab_selected = opts.tab_selected;
618        }
619        if opts.top_left_cell.is_some() {
620            view.top_left_cell = opts.top_left_cell.clone();
621        }
622        if let Some(ref v) = opts.view {
623            if in_str_slice(&["normal", "pageLayout", "pageBreakPreview"], v, true) != -1 {
624                view.view = opts.view.clone();
625            }
626        }
627        if opts.window_protection.is_some() {
628            view.window_protection = opts.window_protection;
629        }
630        if let Some(zoom) = opts.zoom_scale {
631            if zoom >= 10.0 && zoom <= 400.0 {
632                view.zoom_scale = Some(zoom);
633            }
634        }
635        self.sheet.insert(path, ws);
636        Ok(())
637    }
638
639    /// Get worksheet view properties from the given sheet view. The view index may
640    /// be negative and counts backward from the last view.
641    pub fn get_sheet_view(&self, sheet: &str, view_index: i32) -> Result<SheetViewOptions> {
642        let ws = self.work_sheet_reader(sheet)?;
643        let views = ws.sheet_views.unwrap_or_else(|| XlsxSheetViews {
644            sheet_view: vec![XlsxSheetView::default()],
645        });
646        let len = views.sheet_view.len();
647        let idx = if view_index < 0 {
648            if view_index < -(len as i32) {
649                return Err(crate::errors::new_view_idx_error(view_index).into());
650            }
651            (len as i32 + view_index) as usize
652        } else if view_index >= len as i32 {
653            return Err(crate::errors::new_view_idx_error(view_index).into());
654        } else {
655            view_index as usize
656        };
657        let view = &views.sheet_view[idx];
658        let mut opts = SheetViewOptions {
659            default_grid_color: Some(true),
660            show_grid_lines: Some(true),
661            show_row_col_headers: Some(true),
662            show_ruler: Some(true),
663            show_zeros: Some(true),
664            view: Some("normal".to_string()),
665            zoom_scale: Some(100.0),
666            ..Default::default()
667        };
668        if let Some(v) = view.default_grid_color {
669            opts.default_grid_color = Some(v);
670        }
671        opts.right_to_left = Some(view.right_to_left.unwrap_or(false));
672        opts.show_formulas = Some(view.show_formulas.unwrap_or(false));
673        if let Some(v) = view.show_grid_lines {
674            opts.show_grid_lines = Some(v);
675        }
676        if let Some(v) = view.show_row_col_headers {
677            opts.show_row_col_headers = Some(v);
678        }
679        if let Some(v) = view.show_ruler {
680            opts.show_ruler = Some(v);
681        }
682        if let Some(v) = view.show_white_space {
683            opts.show_white_space = Some(v);
684        }
685        if let Some(v) = view.show_zeros {
686            opts.show_zeros = Some(v);
687        }
688        opts.show_outline_symbols = Some(view.show_outline_symbols.unwrap_or(false));
689        opts.tab_selected = Some(view.tab_selected.unwrap_or(false));
690        opts.top_left_cell = Some(view.top_left_cell.clone().unwrap_or_default());
691        opts.window_protection = Some(view.window_protection.unwrap_or(false));
692        if let Some(ref v) = view.view {
693            if !v.is_empty() {
694                opts.view = Some(v.clone());
695            }
696        }
697        if let Some(zoom) = view.zoom_scale {
698            if zoom >= 10.0 && zoom <= 400.0 {
699                opts.zoom_scale = Some(zoom);
700            }
701        }
702        Ok(opts)
703    }
704
705    /// Set page layout properties.
706    pub fn set_page_layout(&self, sheet: &str, opts: &PageLayoutOptions) -> Result<()> {
707        let path = self
708            .get_sheet_xml_path(sheet)
709            .ok_or_else(|| ErrSheetNotExist {
710                sheet_name: sheet.to_string(),
711            })?;
712        let mut ws = self.work_sheet_reader(sheet)?;
713        if ws.page_setup.is_none() {
714            ws.page_setup = Some(XlsxPageSetUp::default());
715        }
716        let ps = ws.page_setup.as_mut().unwrap();
717        if opts.black_and_white.is_some() {
718            ps.black_and_white = opts.black_and_white;
719        }
720        if opts.cell_comments.is_some() {
721            ps.cell_comments = opts.cell_comments.clone();
722        }
723        if opts.copies.is_some() {
724            ps.copies = opts.copies;
725        }
726        if opts.draft.is_some() {
727            ps.draft = opts.draft;
728        }
729        if opts.errors.is_some() {
730            ps.errors = opts.errors.clone();
731        }
732        if opts.first_page_number.is_some() {
733            ps.first_page_number = opts.first_page_number.clone();
734        }
735        if opts.fit_to_height.is_some() {
736            ps.fit_to_height = opts.fit_to_height;
737        }
738        if opts.fit_to_width.is_some() {
739            ps.fit_to_width = opts.fit_to_width;
740        }
741        if opts.horizontal_dpi.is_some() {
742            ps.horizontal_dpi = opts.horizontal_dpi.clone();
743        }
744        if opts.orientation.is_some() {
745            ps.orientation = opts.orientation.clone();
746        }
747        if opts.page_order.is_some() {
748            ps.page_order = opts.page_order.clone();
749        }
750        if opts.paper_size.is_some() {
751            ps.paper_size = opts.paper_size;
752        }
753        if opts.scale.is_some() {
754            ps.scale = opts.scale;
755        }
756        if opts.use_first_page_number.is_some() {
757            ps.use_first_page_number = opts.use_first_page_number;
758        }
759        if opts.use_printer_defaults.is_some() {
760            ps.use_printer_defaults = opts.use_printer_defaults;
761        }
762        if opts.vertical_dpi.is_some() {
763            ps.vertical_dpi = opts.vertical_dpi.clone();
764        }
765        self.sheet.insert(path, ws);
766        Ok(())
767    }
768
769    /// Get page layout properties.
770    pub fn get_page_layout(&self, sheet: &str) -> Result<PageLayoutOptions> {
771        let ws = self.work_sheet_reader(sheet)?;
772        let mut opts = PageLayoutOptions::default();
773        if let Some(ps) = ws.page_setup {
774            opts.black_and_white = ps.black_and_white;
775            opts.cell_comments = ps.cell_comments;
776            opts.copies = ps.copies;
777            opts.draft = ps.draft;
778            opts.errors = ps.errors;
779            opts.first_page_number = ps.first_page_number;
780            opts.fit_to_height = ps.fit_to_height;
781            opts.fit_to_width = ps.fit_to_width;
782            opts.horizontal_dpi = ps.horizontal_dpi;
783            opts.orientation = ps.orientation;
784            opts.page_order = ps.page_order;
785            opts.paper_size = ps.paper_size;
786            opts.scale = ps.scale;
787            opts.use_first_page_number = ps.use_first_page_number;
788            opts.use_printer_defaults = ps.use_printer_defaults;
789            opts.vertical_dpi = ps.vertical_dpi;
790        }
791        Ok(opts)
792    }
793
794    /// Set page margins.
795    pub fn set_page_margins(&self, sheet: &str, opts: &PageMarginsOptions) -> Result<()> {
796        let path = self
797            .get_sheet_xml_path(sheet)
798            .ok_or_else(|| ErrSheetNotExist {
799                sheet_name: sheet.to_string(),
800            })?;
801        let mut ws = self.work_sheet_reader(sheet)?;
802        let margins_needed = opts.left.is_some()
803            || opts.right.is_some()
804            || opts.top.is_some()
805            || opts.bottom.is_some()
806            || opts.header.is_some()
807            || opts.footer.is_some();
808        if margins_needed {
809            if ws.page_margins.is_none() {
810                ws.page_margins = Some(XlsxPageMargins::default());
811            }
812            let pm = ws.page_margins.as_mut().unwrap();
813            if let Some(v) = opts.left {
814                pm.left = v;
815            }
816            if let Some(v) = opts.right {
817                pm.right = v;
818            }
819            if let Some(v) = opts.top {
820                pm.top = v;
821            }
822            if let Some(v) = opts.bottom {
823                pm.bottom = v;
824            }
825            if let Some(v) = opts.header {
826                pm.header = v;
827            }
828            if let Some(v) = opts.footer {
829                pm.footer = v;
830            }
831        }
832        if opts.horizontally.is_some() || opts.vertically.is_some() {
833            if ws.print_options.is_none() {
834                ws.print_options = Some(XlsxPrintOptions::default());
835            }
836            let po = ws.print_options.as_mut().unwrap();
837            if let Some(v) = opts.horizontally {
838                po.horizontal_centered = Some(v);
839            }
840            if let Some(v) = opts.vertically {
841                po.vertical_centered = Some(v);
842            }
843        }
844        self.sheet.insert(path, ws);
845        Ok(())
846    }
847
848    /// Get page margins.
849    pub fn get_page_margins(&self, sheet: &str) -> Result<PageMarginsOptions> {
850        let ws = self.work_sheet_reader(sheet)?;
851        let mut opts = PageMarginsOptions {
852            left: Some(0.7),
853            right: Some(0.7),
854            top: Some(0.75),
855            bottom: Some(0.75),
856            header: Some(0.3),
857            footer: Some(0.3),
858            ..Default::default()
859        };
860        if let Some(pm) = ws.page_margins {
861            opts.left = Some(pm.left);
862            opts.right = Some(pm.right);
863            opts.top = Some(pm.top);
864            opts.bottom = Some(pm.bottom);
865            opts.header = Some(pm.header);
866            opts.footer = Some(pm.footer);
867        }
868        if let Some(po) = ws.print_options {
869            opts.horizontally = po.horizontal_centered;
870            opts.vertically = po.vertical_centered;
871        }
872        Ok(opts)
873    }
874
875    /// Set header/footer text.
876    pub fn set_header_footer(&self, sheet: &str, opts: &HeaderFooterOptions) -> Result<()> {
877        let path = self
878            .get_sheet_xml_path(sheet)
879            .ok_or_else(|| ErrSheetNotExist {
880                sheet_name: sheet.to_string(),
881            })?;
882        let mut ws = self.work_sheet_reader(sheet)?;
883        for (field, value) in [
884            ("OddHeader", opts.odd_header.as_deref()),
885            ("OddFooter", opts.odd_footer.as_deref()),
886            ("EvenHeader", opts.even_header.as_deref()),
887            ("EvenFooter", opts.even_footer.as_deref()),
888            ("FirstHeader", opts.first_header.as_deref()),
889            ("FirstFooter", opts.first_footer.as_deref()),
890        ] {
891            if let Some(v) = value {
892                if count_utf16_string(v) > MAX_FIELD_LENGTH {
893                    return Err(new_field_length_error(field).into());
894                }
895            }
896        }
897        if ws.header_footer.is_none() {
898            ws.header_footer = Some(XlsxHeaderFooter::default());
899        }
900        let hf = ws.header_footer.as_mut().unwrap();
901        if opts.different_first.is_some() {
902            hf.different_first = opts.different_first;
903        }
904        if opts.different_odd_even.is_some() {
905            hf.different_odd_even = opts.different_odd_even;
906        }
907        if opts.scale_with_doc.is_some() {
908            hf.scale_with_doc = opts.scale_with_doc;
909        }
910        if opts.align_with_margins.is_some() {
911            hf.align_with_margins = opts.align_with_margins;
912        }
913        if opts.odd_header.is_some() {
914            hf.odd_header = opts.odd_header.clone();
915        }
916        if opts.odd_footer.is_some() {
917            hf.odd_footer = opts.odd_footer.clone();
918        }
919        if opts.even_header.is_some() {
920            hf.even_header = opts.even_header.clone();
921        }
922        if opts.even_footer.is_some() {
923            hf.even_footer = opts.even_footer.clone();
924        }
925        if opts.first_header.is_some() {
926            hf.first_header = opts.first_header.clone();
927        }
928        if opts.first_footer.is_some() {
929            hf.first_footer = opts.first_footer.clone();
930        }
931        self.sheet.insert(path, ws);
932        Ok(())
933    }
934
935    /// Get header/footer text.
936    pub fn get_header_footer(&self, sheet: &str) -> Result<HeaderFooterOptions> {
937        let ws = self.work_sheet_reader(sheet)?;
938        let mut opts = HeaderFooterOptions::default();
939        if let Some(hf) = ws.header_footer {
940            opts.different_first = hf.different_first;
941            opts.different_odd_even = hf.different_odd_even;
942            opts.scale_with_doc = hf.scale_with_doc;
943            opts.align_with_margins = hf.align_with_margins;
944            opts.odd_header = hf.odd_header;
945            opts.odd_footer = hf.odd_footer;
946            opts.even_header = hf.even_header;
947            opts.even_footer = hf.even_footer;
948            opts.first_header = hf.first_header;
949            opts.first_footer = hf.first_footer;
950        }
951        Ok(opts)
952    }
953
954    /// Set the worksheet name by given source and target worksheet names.
955    pub fn set_sheet_name(&self, source: &str, target: &str) -> Result<()> {
956        crate::excelize::check_sheet_name(target)?;
957        if source.eq_ignore_ascii_case(target) {
958            return Ok(());
959        }
960        self.clear_calc_cache();
961        let mut wb = self.workbook_reader()?;
962        for k in 0..wb.sheets.sheet.len() {
963            if wb.sheets.sheet[k]
964                .name
965                .as_deref()
966                .map(|n| n.eq_ignore_ascii_case(source))
967                .unwrap_or(false)
968            {
969                wb.sheets.sheet[k].name = Some(target.to_string());
970                let mut map = self.sheet_map.lock().unwrap();
971                if let Some(path) = map.get(source).cloned() {
972                    map.remove(source);
973                    map.insert(target.to_string(), path);
974                }
975            }
976        }
977        if let Some(names) = wb.defined_names.as_mut() {
978            for dn in &mut names.defined_name {
979                dn.data = adjust_range_sheet_name(&dn.data, source, target);
980            }
981        }
982        *self.workbook.lock().unwrap() = Some(wb);
983        Ok(())
984    }
985
986    /// Set background picture by given worksheet name and file path.
987    pub fn set_sheet_background(&self, sheet: &str, picture: &str) -> Result<()> {
988        let ext = Path::new(picture)
989            .extension()
990            .and_then(|e| e.to_str())
991            .unwrap_or("")
992            .to_string();
993        let file = std::fs::read(picture)?;
994        self.set_sheet_background_from_bytes(sheet, &ext, &file)
995    }
996
997    /// Set background picture by given worksheet name, extension name and image data.
998    pub fn set_sheet_background_from_bytes(
999        &self,
1000        sheet: &str,
1001        extension: &str,
1002        picture: &[u8],
1003    ) -> Result<()> {
1004        if picture.is_empty() {
1005            return Err(Box::new(ErrParameterInvalid));
1006        }
1007        let ext = if !extension.is_empty() {
1008            format!(".{extension}")
1009        } else {
1010            String::new()
1011        };
1012        let image_type = image_extension_map()
1013            .get(&ext.to_lowercase())
1014            .cloned()
1015            .ok_or(ErrImgExt)?;
1016        let name = self.add_media(picture, &image_type);
1017        let sheet_xml_path = self
1018            .get_sheet_xml_path(sheet)
1019            .ok_or_else(|| ErrSheetNotExist {
1020                sheet_name: sheet.to_string(),
1021            })?;
1022        let sheet_rels = format!(
1023            "xl/worksheets/_rels/{}.rels",
1024            sheet_xml_path.trim_start_matches("xl/worksheets/")
1025        );
1026        let target = name.replacen("xl", "..", 1);
1027        let r_id = self.add_rels(&sheet_rels, SOURCE_RELATIONSHIP_IMAGE, &target, "");
1028        self.add_sheet_picture(sheet, r_id)?;
1029        self.add_sheet_name_space(sheet, SOURCE_RELATIONSHIP);
1030        set_content_type_part_image_extensions(self)
1031    }
1032
1033    /// Move a sheet to a specified position in the workbook.
1034    /// Indices are 1-based and the source sheet is placed before the target sheet.
1035    pub fn move_sheet(&self, source: &str, target: &str) -> Result<()> {
1036        if source.eq_ignore_ascii_case(target) {
1037            return Ok(());
1038        }
1039        let mut wb = self.workbook_reader()?;
1040        let source_idx = self.get_sheet_index(source)?;
1041        let target_idx = self.get_sheet_index(target)?;
1042        if source_idx < 1 {
1043            return Err(Box::new(ErrSheetNotExist {
1044                sheet_name: source.to_string(),
1045            }));
1046        }
1047        if target_idx < 1 {
1048            return Err(Box::new(ErrSheetNotExist {
1049                sheet_name: target.to_string(),
1050            }));
1051        }
1052        let _ = self.ungroup_sheets();
1053        let active_sheet_name = self.get_sheet_name(self.get_active_sheet_index()?)?;
1054        let source_sheet = wb.sheets.sheet.remove(source_idx as usize - 1);
1055        let mut target_idx = target_idx;
1056        if target_idx > source_idx {
1057            target_idx -= 1;
1058        }
1059        wb.sheets
1060            .sheet
1061            .insert(target_idx as usize - 1, source_sheet);
1062        *self.workbook.lock().unwrap() = Some(wb);
1063        let active_idx = self.get_sheet_index(&active_sheet_name)?;
1064        self.set_active_sheet(active_idx)?;
1065        Ok(())
1066    }
1067
1068    /// Duplicate a worksheet by source and target 1-based sheet indices.
1069    pub fn copy_sheet(&self, from: i32, to: i32) -> Result<()> {
1070        if from < 1 || to < 1 || from == to {
1071            return Err(Box::new(ErrSheetIdx));
1072        }
1073        let from_name = self.get_sheet_name(from)?;
1074        let to_name = self.get_sheet_name(to)?;
1075        if from_name.is_empty() || to_name.is_empty() {
1076            return Err(Box::new(ErrSheetIdx));
1077        }
1078        self.clear_calc_cache();
1079        let ws = self.work_sheet_reader(&from_name)?;
1080        let mut worksheet = ws.clone();
1081        if let Some(views) = worksheet.sheet_views.as_mut() {
1082            if let Some(view) = views.sheet_view.first_mut() {
1083                view.tab_selected = Some(false);
1084            }
1085        }
1086        worksheet.drawing = None;
1087        worksheet.table_parts = None;
1088        worksheet.page_setup = None;
1089        let to_sheet_id = self.get_sheet_id(&to_name);
1090        let sheet_xml_path = format!("xl/worksheets/sheet{to_sheet_id}.xml");
1091        self.sheet.insert(sheet_xml_path.clone(), worksheet);
1092        let to_rels = format!("xl/worksheets/_rels/sheet{to_sheet_id}.xml.rels");
1093        let from_sheet_id = self.get_sheet_id(&from_name);
1094        let from_rels = format!("xl/worksheets/_rels/sheet{from_sheet_id}.xml.rels");
1095        if let Some(rels) = self.pkg.get(&from_rels) {
1096            self.pkg.insert(to_rels, rels.clone());
1097        }
1098        let from_sheet_xml_path = self.get_sheet_xml_path(&from_name).unwrap_or_default();
1099        if let Some(attrs) = self.xml_attr.get(&from_sheet_xml_path) {
1100            self.xml_attr.insert(sheet_xml_path, attrs.clone());
1101        }
1102        Ok(())
1103    }
1104
1105    /// Set worksheet visible by given worksheet name.
1106    pub fn set_sheet_visible(
1107        &self,
1108        sheet: &str,
1109        visible: bool,
1110        very_hidden: Option<bool>,
1111    ) -> Result<()> {
1112        crate::excelize::check_sheet_name(sheet)?;
1113        let mut wb = self.workbook_reader()?;
1114        if visible {
1115            for k in 0..wb.sheets.sheet.len() {
1116                if wb.sheets.sheet[k]
1117                    .name
1118                    .as_deref()
1119                    .map(|n| n.eq_ignore_ascii_case(sheet))
1120                    .unwrap_or(false)
1121                {
1122                    wb.sheets.sheet[k].state = None;
1123                }
1124            }
1125            *self.workbook.lock().unwrap() = Some(wb);
1126            return Ok(());
1127        }
1128        let state = get_sheet_state(false, very_hidden);
1129        let mut count = 0;
1130        for s in &wb.sheets.sheet {
1131            if s.state.as_deref().unwrap_or("") != state {
1132                count += 1;
1133            }
1134        }
1135        for k in 0..wb.sheets.sheet.len() {
1136            let name = wb.sheets.sheet[k].name.clone().unwrap_or_default();
1137            let ws = self.work_sheet_reader(&name)?;
1138            let mut tab_selected = false;
1139            if let Some(views) = ws.sheet_views.as_ref() {
1140                if let Some(view) = views.sheet_view.first() {
1141                    tab_selected = view.tab_selected.unwrap_or(false);
1142                }
1143            }
1144            if name.eq_ignore_ascii_case(sheet) && count > 1 && !tab_selected {
1145                wb.sheets.sheet[k].state = Some(state.to_string());
1146            }
1147        }
1148        *self.workbook.lock().unwrap() = Some(wb);
1149        Ok(())
1150    }
1151
1152    /// Get worksheet visible by given worksheet name.
1153    pub fn get_sheet_visible(&self, sheet: &str) -> Result<bool> {
1154        crate::excelize::check_sheet_name(sheet)?;
1155        let wb = self.workbook_reader()?;
1156        for s in &wb.sheets.sheet {
1157            if s.name
1158                .as_deref()
1159                .map(|n| n.eq_ignore_ascii_case(sheet))
1160                .unwrap_or(false)
1161            {
1162                let state = s.state.as_deref().unwrap_or("");
1163                return Ok(state.is_empty() || state.eq_ignore_ascii_case("visible"));
1164            }
1165        }
1166        Ok(false)
1167    }
1168
1169    /// Create and remove freeze panes and split panes by given worksheet name and panes options.
1170    pub fn set_panes(&self, sheet: &str, panes: &Panes) -> Result<()> {
1171        let path = self
1172            .get_sheet_xml_path(sheet)
1173            .ok_or_else(|| ErrSheetNotExist {
1174                sheet_name: sheet.to_string(),
1175            })?;
1176        let mut ws = self.work_sheet_reader(sheet)?;
1177        set_panes_ws(&mut ws, panes)?;
1178        self.sheet.insert(path, ws);
1179        Ok(())
1180    }
1181
1182    /// Get freeze panes, split panes, and worksheet views by given worksheet name.
1183    pub fn get_panes(&self, sheet: &str) -> Result<Panes> {
1184        let ws = self.work_sheet_reader(sheet)?;
1185        Ok(get_panes_ws(&ws))
1186    }
1187
1188    /// Search cell reference by given worksheet name, cell value, and regular expression.
1189    pub fn search_sheet(&self, sheet: &str, value: &str, reg: Option<bool>) -> Result<Vec<String>> {
1190        crate::excelize::check_sheet_name(sheet)?;
1191        let reg_search = reg.unwrap_or(false);
1192        let name = self
1193            .get_sheet_xml_path(sheet)
1194            .ok_or_else(|| ErrSheetNotExist {
1195                sheet_name: sheet.to_string(),
1196            })?;
1197        if let Some(ws) = self.sheet.get(&name) {
1198            let ws = ws.clone();
1199            self.save_file_list(&name, &worksheet_bytes(&ws));
1200        }
1201        search_sheet_impl(self, sheet, value, reg_search)
1202    }
1203
1204    /// Protect worksheet to prevent accidental or deliberate changes.
1205    pub fn protect_sheet(&self, sheet: &str, opts: &SheetProtectionOptions) -> Result<()> {
1206        let path = self
1207            .get_sheet_xml_path(sheet)
1208            .ok_or_else(|| ErrSheetNotExist {
1209                sheet_name: sheet.to_string(),
1210            })?;
1211        let mut ws = self.work_sheet_reader(sheet)?;
1212        let mut sp = XlsxSheetProtection {
1213            auto_filter: !opts.auto_filter,
1214            delete_columns: !opts.delete_columns,
1215            delete_rows: !opts.delete_rows,
1216            format_cells: !opts.format_cells,
1217            format_columns: !opts.format_columns,
1218            format_rows: !opts.format_rows,
1219            insert_columns: !opts.insert_columns,
1220            insert_hyperlinks: !opts.insert_hyperlinks,
1221            insert_rows: !opts.insert_rows,
1222            objects: !opts.edit_objects,
1223            pivot_tables: !opts.pivot_tables,
1224            scenarios: !opts.edit_scenarios,
1225            select_locked_cells: !opts.select_locked_cells,
1226            select_unlocked_cells: !opts.select_unlocked_cells,
1227            sheet: true,
1228            sort: !opts.sort,
1229            ..Default::default()
1230        };
1231        if !opts.password.is_empty() {
1232            if opts.algorithm_name.is_empty() {
1233                sp.password = Some(gen_sheet_passwd(&opts.password));
1234            } else {
1235                let (hash_value, salt_value) =
1236                    gen_iso_passwd_hash(&opts.password, &opts.algorithm_name, "", 100_000)?;
1237                sp.password = None;
1238                sp.algorithm_name = Some(opts.algorithm_name.clone());
1239                sp.salt_value = Some(salt_value);
1240                sp.hash_value = Some(hash_value);
1241                sp.spin_count = Some(100_000);
1242            }
1243        }
1244        ws.sheet_protection = Some(sp);
1245        self.sheet.insert(path, ws);
1246        Ok(())
1247    }
1248
1249    /// Remove protection for a sheet.
1250    pub fn unprotect_sheet(&self, sheet: &str, password: Option<&str>) -> Result<()> {
1251        let path = self
1252            .get_sheet_xml_path(sheet)
1253            .ok_or_else(|| ErrSheetNotExist {
1254                sheet_name: sheet.to_string(),
1255            })?;
1256        let mut ws = self.work_sheet_reader(sheet)?;
1257        if let Some(passwd) = password {
1258            let sp = ws.sheet_protection.as_ref().ok_or(ErrUnprotectSheet)?;
1259            if sp.algorithm_name.as_deref().unwrap_or("").is_empty() {
1260                if sp.password.as_deref().unwrap_or("") != gen_sheet_passwd(passwd) {
1261                    return Err(Box::new(ErrUnprotectSheetPassword));
1262                }
1263            } else {
1264                let (hash_value, _) = gen_iso_passwd_hash(
1265                    passwd,
1266                    sp.algorithm_name.as_deref().unwrap_or(""),
1267                    sp.salt_value.as_deref().unwrap_or(""),
1268                    sp.spin_count.unwrap_or(0) as i32,
1269                )?;
1270                if sp.hash_value.as_deref().unwrap_or("") != hash_value {
1271                    return Err(Box::new(ErrUnprotectSheetPassword));
1272                }
1273            }
1274        }
1275        ws.sheet_protection = None;
1276        self.sheet.insert(path, ws);
1277        Ok(())
1278    }
1279
1280    /// Get worksheet protection settings by given worksheet name.
1281    pub fn get_sheet_protection(&self, sheet: &str) -> Result<SheetProtectionOptions> {
1282        let ws = self.work_sheet_reader(sheet)?;
1283        let mut opts = SheetProtectionOptions::default();
1284        if let Some(sp) = ws.sheet_protection {
1285            opts.algorithm_name = sp.algorithm_name.unwrap_or_default();
1286            opts.auto_filter = !sp.auto_filter;
1287            opts.delete_columns = !sp.delete_columns;
1288            opts.delete_rows = !sp.delete_rows;
1289            opts.edit_objects = !sp.objects;
1290            opts.edit_scenarios = !sp.scenarios;
1291            opts.format_cells = !sp.format_cells;
1292            opts.format_columns = !sp.format_columns;
1293            opts.format_rows = !sp.format_rows;
1294            opts.insert_columns = !sp.insert_columns;
1295            opts.insert_hyperlinks = !sp.insert_hyperlinks;
1296            opts.insert_rows = !sp.insert_rows;
1297            opts.pivot_tables = !sp.pivot_tables;
1298            opts.select_locked_cells = !sp.select_locked_cells;
1299            opts.select_unlocked_cells = !sp.select_unlocked_cells;
1300            opts.sort = !sp.sort;
1301        }
1302        Ok(opts)
1303    }
1304
1305    /// Group worksheets by given worksheet names.
1306    pub fn group_sheets(&self, sheets: &[String]) -> Result<()> {
1307        let active_sheet = self.get_active_sheet_index()?;
1308        let sheet_list = self.get_sheet_list();
1309        let mut in_active_sheet = false;
1310        for (idx, sheet_name) in sheet_list.iter().enumerate() {
1311            for s in sheets {
1312                if s.eq_ignore_ascii_case(sheet_name) && (idx + 1) as i32 == active_sheet {
1313                    in_active_sheet = true;
1314                }
1315            }
1316        }
1317        if !in_active_sheet {
1318            return Err(Box::new(ErrGroupSheets));
1319        }
1320        for sheet in sheets {
1321            let mut ws = self.work_sheet_reader(sheet)?;
1322            let path = self.get_sheet_xml_path(sheet).unwrap_or_default();
1323            if let Some(views) = ws.sheet_views.as_mut() {
1324                for view in &mut views.sheet_view {
1325                    view.tab_selected = Some(true);
1326                }
1327            }
1328            self.sheet.insert(path, ws);
1329        }
1330        Ok(())
1331    }
1332
1333    /// Ungroup worksheets.
1334    pub fn ungroup_sheets(&self) -> Result<()> {
1335        let active_sheet = self.get_active_sheet_index()?;
1336        for (index, sheet) in self.get_sheet_list().iter().enumerate() {
1337            if active_sheet == (index + 1) as i32 {
1338                continue;
1339            }
1340            let mut ws = self.work_sheet_reader(sheet)?;
1341            let path = self.get_sheet_xml_path(sheet).unwrap_or_default();
1342            if let Some(views) = ws.sheet_views.as_mut() {
1343                for view in &mut views.sheet_view {
1344                    view.tab_selected = Some(false);
1345                }
1346            }
1347            self.sheet.insert(path, ws);
1348        }
1349        Ok(())
1350    }
1351
1352    /// Create a page break by given worksheet name and cell reference.
1353    pub fn insert_page_break(&self, sheet: &str, cell: &str) -> Result<()> {
1354        let path = self
1355            .get_sheet_xml_path(sheet)
1356            .ok_or_else(|| ErrSheetNotExist {
1357                sheet_name: sheet.to_string(),
1358            })?;
1359        let mut ws = self.work_sheet_reader(sheet)?;
1360        insert_page_break_ws(&mut ws, cell)?;
1361        self.sheet.insert(path, ws);
1362        Ok(())
1363    }
1364
1365    /// Remove a page break by given worksheet name and cell reference.
1366    pub fn remove_page_break(&self, sheet: &str, cell: &str) -> Result<()> {
1367        let path = self
1368            .get_sheet_xml_path(sheet)
1369            .ok_or_else(|| ErrSheetNotExist {
1370                sheet_name: sheet.to_string(),
1371            })?;
1372        let mut ws = self.work_sheet_reader(sheet)?;
1373        remove_page_break_ws(&mut ws, cell)?;
1374        self.sheet.insert(path, ws);
1375        Ok(())
1376    }
1377
1378    /// Set or remove the used range of the worksheet by a given range reference.
1379    pub fn set_sheet_dimension(&self, sheet: &str, range_ref: &str) -> Result<()> {
1380        let path = self
1381            .get_sheet_xml_path(sheet)
1382            .ok_or_else(|| ErrSheetNotExist {
1383                sheet_name: sheet.to_string(),
1384            })?;
1385        let mut ws = self.work_sheet_reader(sheet)?;
1386        if range_ref.is_empty() {
1387            ws.dimension = None;
1388            self.sheet.insert(path, ws);
1389            return Ok(());
1390        }
1391        let parts: Vec<&str> = range_ref.split(':').collect();
1392        if parts.len() == 1 {
1393            let _ = cell_name_to_coordinates(range_ref)?;
1394            ws.dimension = Some(XlsxDimension {
1395                r#ref: range_ref.to_uppercase(),
1396            });
1397            self.sheet.insert(path, ws);
1398            return Ok(());
1399        }
1400        if parts.len() != 2 {
1401            return Err(Box::new(ErrParameterInvalid));
1402        }
1403        let mut coordinates = range_ref_to_coordinates(range_ref)?;
1404        sort_coordinates(&mut coordinates)?;
1405        let r#ref = coordinates_to_range_ref(&coordinates, false)?;
1406        ws.dimension = Some(XlsxDimension { r#ref });
1407        self.sheet.insert(path, ws);
1408        Ok(())
1409    }
1410
1411    /// Get the used range of the worksheet.
1412    pub fn get_sheet_dimension(&self, sheet: &str) -> Result<String> {
1413        crate::excelize::check_sheet_name(sheet)?;
1414        let name = self
1415            .get_sheet_xml_path(sheet)
1416            .ok_or_else(|| ErrSheetNotExist {
1417                sheet_name: sheet.to_string(),
1418            })?;
1419        if let Some(ws) = self.sheet.get(&name) {
1420            return Ok(ws
1421                .dimension
1422                .as_ref()
1423                .map(|d| d.r#ref.clone())
1424                .unwrap_or_default());
1425        }
1426        let data = self.read_bytes(&name);
1427        let s = String::from_utf8_lossy(&data);
1428        if let Some(start) = s.find("<dimension") {
1429            if let Some(end) = s[start..].find('>') {
1430                let tag = &s[start..start + end + 1];
1431                if let Some(pos) = tag.find("ref=\"") {
1432                    let rest = &tag[pos + 6..];
1433                    if let Some(q) = rest.find('"') {
1434                        return Ok(rest[..q].to_string());
1435                    }
1436                }
1437            }
1438        }
1439        Ok(String::new())
1440    }
1441
1442    /// Ignore error for a range of cells.
1443    pub fn add_ignored_errors(
1444        &self,
1445        sheet: &str,
1446        range_ref: &str,
1447        ignored_errors_type: IgnoredErrorsType,
1448    ) -> Result<()> {
1449        if range_ref.is_empty() {
1450            return Err(Box::new(ErrParameterInvalid));
1451        }
1452        let path = self
1453            .get_sheet_xml_path(sheet)
1454            .ok_or_else(|| ErrSheetNotExist {
1455                sheet_name: sheet.to_string(),
1456            })?;
1457        let mut ws = self.work_sheet_reader(sheet)?;
1458        if ws.ignored_errors.is_none() {
1459            ws.ignored_errors = Some(XlsxIgnoredErrors::default());
1460        }
1461        let ie = ignored_error_for_type(range_ref, ignored_errors_type);
1462        let ignored = ws.ignored_errors.as_mut().unwrap();
1463        if ignored
1464            .ignored_error
1465            .iter()
1466            .any(|e| e.sqref == ie.sqref && same_ignored_error_flags(e, &ie))
1467        {
1468            self.sheet.insert(path, ws);
1469            return Ok(());
1470        }
1471        ignored.ignored_error.push(ie);
1472        self.sheet.insert(path, ws);
1473        Ok(())
1474    }
1475}
1476
1477// ------------------------------------------------------------------
1478// Internal helpers
1479// ------------------------------------------------------------------
1480
1481impl File {
1482    /// Build a map of worksheet name → XML path from the workbook and its rels.
1483    pub fn get_sheet_name_to_path_map(&self) -> Result<HashMap<String, String>> {
1484        let mut map = HashMap::new();
1485        let wb = self.workbook_reader()?;
1486        let rels = self.rels_reader(&self.get_workbook_rels_path())?;
1487        let Some(rels) = rels else {
1488            return Ok(map);
1489        };
1490        for sheet in &wb.sheets.sheet {
1491            let Some(name) = &sheet.name else { continue };
1492            let id = sheet.id.as_ref().or(sheet.plain_id.as_ref());
1493            let Some(id) = id else { continue };
1494            for rel in &rels.relationships {
1495                if &rel.id == id {
1496                    let sheet_xml = self.get_worksheet_path(&rel.target);
1497                    if self.pkg.contains_key(&sheet_xml) || self.temp_files.contains_key(&sheet_xml)
1498                    {
1499                        map.insert(name.clone(), sheet_xml);
1500                    }
1501                }
1502            }
1503        }
1504        Ok(map)
1505    }
1506
1507    /// Get worksheets, chart sheets and dialog sheets ID and name map of the workbook.
1508    pub fn get_sheet_map(&self) -> HashMap<i32, String> {
1509        let mut sheet_map = HashMap::new();
1510        if let Ok(wb) = self.workbook_reader() {
1511            for sheet in &wb.sheets.sheet {
1512                if let (Some(id), Some(name)) = (sheet.sheet_id, sheet.name.clone()) {
1513                    sheet_map.insert(id as i32, name);
1514                }
1515            }
1516        }
1517        sheet_map
1518    }
1519
1520    /// Convert a relationship id to a 0-based sheet index.
1521    pub fn rel_id_to_sheet_id(&self, r_id: &str) -> Result<i32> {
1522        let wb = self.workbook_reader()?;
1523        for (i, sheet) in wb.sheets.sheet.iter().enumerate() {
1524            let id = sheet.id.as_deref().or(sheet.plain_id.as_deref());
1525            if id == Some(r_id) {
1526                return Ok(i as i32);
1527            }
1528        }
1529        Err(Box::new(ErrSheetIdx))
1530    }
1531}
1532
1533fn new_worksheet() -> XlsxWorksheet {
1534    XlsxWorksheet {
1535        sheet_format_pr: Some(XlsxSheetFormatPr {
1536            default_row_height: 15.0,
1537            ..Default::default()
1538        }),
1539        ..Default::default()
1540    }
1541}
1542
1543fn worksheet_bytes(ws: &XlsxWorksheet) -> Vec<u8> {
1544    use quick_xml::se::to_string;
1545    let body = to_string(ws).unwrap_or_default().into_bytes();
1546    [crate::constants::XML_HEADER.as_bytes(), &body].concat()
1547}
1548
1549impl File {
1550    /// Set a picture reference on a worksheet for use as a background image.
1551    pub(crate) fn add_sheet_picture(&self, sheet: &str, r_id: i32) -> Result<()> {
1552        let path = self
1553            .get_sheet_xml_path(sheet)
1554            .ok_or_else(|| ErrSheetNotExist {
1555                sheet_name: sheet.to_string(),
1556            })?;
1557        let mut ws = self.work_sheet_reader(sheet)?;
1558        ws.picture = Some(XlsxPicture {
1559            rid: Some(format!("rId{r_id}")),
1560        });
1561        self.sheet.insert(path, ws);
1562        Ok(())
1563    }
1564}
1565
1566fn set_panes_ws(ws: &mut XlsxWorksheet, panes: &Panes) -> Result<()> {
1567    let mut p = XlsxPane {
1568        active_pane: Some(panes.active_pane.clone()),
1569        top_left_cell: Some(panes.top_left_cell.clone()),
1570        x_split: Some(panes.x_split as f64),
1571        y_split: Some(panes.y_split as f64),
1572        ..Default::default()
1573    };
1574    if panes.freeze {
1575        p.state = Some("frozen".to_string());
1576    }
1577    if ws.sheet_views.is_none() {
1578        ws.sheet_views = Some(XlsxSheetViews::default());
1579    }
1580    let views = ws.sheet_views.as_mut().unwrap();
1581    if views.sheet_view.is_empty() {
1582        views.sheet_view.push(XlsxSheetView::default());
1583    }
1584    let idx = views.sheet_view.len() - 1;
1585    if !panes.freeze && !panes.split {
1586        views.sheet_view[idx].pane = None;
1587    } else {
1588        views.sheet_view[idx].pane = Some(p);
1589    }
1590    let mut selection = Vec::new();
1591    for s in &panes.selection {
1592        selection.push(XlsxSelection {
1593            active_cell: Some(s.active_cell.clone()),
1594            pane: Some(s.pane.clone()),
1595            sqref: Some(s.sqref.clone()),
1596            ..Default::default()
1597        });
1598    }
1599    views.sheet_view[idx].selection = selection;
1600    Ok(())
1601}
1602
1603fn get_panes_ws(ws: &XlsxWorksheet) -> Panes {
1604    let mut panes = Panes::default();
1605    let Some(views) = ws.sheet_views.as_ref() else {
1606        return panes;
1607    };
1608    let Some(view) = views.sheet_view.last() else {
1609        return panes;
1610    };
1611    for s in &view.selection {
1612        panes.selection.push(Selection {
1613            sqref: s.sqref.clone().unwrap_or_default(),
1614            active_cell: s.active_cell.clone().unwrap_or_default(),
1615            pane: s.pane.clone().unwrap_or_default(),
1616        });
1617    }
1618    let Some(p) = view.pane.as_ref() else {
1619        return panes;
1620    };
1621    panes.active_pane = p.active_pane.clone().unwrap_or_default();
1622    if p.state.as_deref() == Some("frozen") {
1623        panes.freeze = true;
1624    }
1625    panes.top_left_cell = p.top_left_cell.clone().unwrap_or_default();
1626    panes.x_split = p.x_split.unwrap_or(0.0) as i64;
1627    panes.y_split = p.y_split.unwrap_or(0.0) as i64;
1628    panes
1629}
1630
1631fn search_sheet_impl(file: &File, sheet: &str, value: &str, reg: bool) -> Result<Vec<String>> {
1632    let name = file
1633        .get_sheet_xml_path(sheet)
1634        .ok_or_else(|| ErrSheetNotExist {
1635            sheet_name: sheet.to_string(),
1636        })?;
1637    let raw = {
1638        let raw = file.read_xml(&name);
1639        if !raw.is_empty() {
1640            raw
1641        } else if let Some(ws) = file.sheet.get(&name) {
1642            worksheet_bytes(&ws)
1643        } else {
1644            file.read_bytes(&name)
1645        }
1646    };
1647    let data = file.apply_charset_transcoder(&raw)?;
1648    let data = namespace_strict_to_transitional(&data);
1649    let sst = file.shared_strings_reader().ok();
1650
1651    let regex = if reg {
1652        Some(regex::Regex::new(value).map_err(|e| e.to_string())?)
1653    } else {
1654        None
1655    };
1656
1657    let mut result = Vec::new();
1658    let mut merge_cells = Vec::new();
1659
1660    let mut reader = Reader::from_reader(Cursor::new(data));
1661    let mut buf = Vec::new();
1662
1663    let mut _current_row = 0;
1664    let mut in_cell = false;
1665    let mut in_v = false;
1666    let mut in_is = false;
1667    let mut in_is_t = false;
1668    let mut cell_ref = String::new();
1669    let mut cell_type = None::<String>;
1670    let mut current_val = String::new();
1671
1672    loop {
1673        match reader.read_event_into(&mut buf) {
1674            Ok(Event::Start(e)) => {
1675                let local_name = e.local_name();
1676                let name = local_name.as_ref();
1677                if name == b"row" {
1678                    if let Ok(Some(attr)) = e.try_get_attribute("r") {
1679                        if let Ok(r_str) = attr.decode_and_unescape_value(reader.decoder()) {
1680                            _current_row = r_str.parse::<i32>().unwrap_or(0);
1681                        }
1682                    }
1683                } else if name == b"c" {
1684                    in_cell = true;
1685                    in_v = false;
1686                    in_is = false;
1687                    in_is_t = false;
1688                    cell_ref.clear();
1689                    cell_type = None;
1690                    current_val.clear();
1691                    if let Ok(Some(attr)) = e.try_get_attribute("r") {
1692                        if let Ok(r) = attr.decode_and_unescape_value(reader.decoder()) {
1693                            cell_ref = r.to_string();
1694                        }
1695                    }
1696                    if let Ok(Some(attr)) = e.try_get_attribute("t") {
1697                        if let Ok(t) = attr.decode_and_unescape_value(reader.decoder()) {
1698                            cell_type = Some(t.to_string());
1699                        }
1700                    }
1701                } else if in_cell && name == b"v" {
1702                    in_v = true;
1703                    current_val.clear();
1704                } else if in_cell && name == b"is" {
1705                    in_is = true;
1706                    current_val.clear();
1707                } else if in_is && name == b"t" {
1708                    in_is_t = true;
1709                }
1710            }
1711            Ok(Event::Empty(e)) => {
1712                let local_name = e.local_name();
1713                let name = local_name.as_ref();
1714                if name == b"c" {
1715                    cell_ref.clear();
1716                    cell_type = None;
1717                    current_val.clear();
1718                    if let Ok(Some(attr)) = e.try_get_attribute("r") {
1719                        if let Ok(r) = attr.decode_and_unescape_value(reader.decoder()) {
1720                            cell_ref = r.to_string();
1721                        }
1722                    }
1723                    if let Ok(Some(attr)) = e.try_get_attribute("t") {
1724                        if let Ok(t) = attr.decode_and_unescape_value(reader.decoder()) {
1725                            cell_type = Some(t.to_string());
1726                        }
1727                    }
1728                    evaluate_cell(
1729                        &mut result,
1730                        &cell_ref,
1731                        cell_type.as_deref(),
1732                        &current_val,
1733                        sst.as_ref(),
1734                        value,
1735                        reg,
1736                        regex.as_ref(),
1737                    );
1738                } else if name == b"mergeCell" {
1739                    if let Ok(Some(attr)) = e.try_get_attribute("ref") {
1740                        if let Ok(r) = attr.decode_and_unescape_value(reader.decoder()) {
1741                            merge_cells.push(r.to_string());
1742                        }
1743                    }
1744                }
1745            }
1746            Ok(Event::Text(e)) => {
1747                if in_v || in_is_t {
1748                    if let Ok(text) = e.unescape() {
1749                        current_val.push_str(&text);
1750                    }
1751                }
1752            }
1753            Ok(Event::End(e)) => {
1754                let local_name = e.local_name();
1755                let name = local_name.as_ref();
1756                if in_cell && name == b"c" {
1757                    evaluate_cell(
1758                        &mut result,
1759                        &cell_ref,
1760                        cell_type.as_deref(),
1761                        &current_val,
1762                        sst.as_ref(),
1763                        value,
1764                        reg,
1765                        regex.as_ref(),
1766                    );
1767                    in_cell = false;
1768                    in_v = false;
1769                    in_is = false;
1770                    in_is_t = false;
1771                    cell_ref.clear();
1772                    cell_type = None;
1773                    current_val.clear();
1774                } else if in_v && name == b"v" {
1775                    in_v = false;
1776                } else if in_is && name == b"is" {
1777                    in_is = false;
1778                    in_is_t = false;
1779                } else if in_is_t && name == b"t" {
1780                    in_is_t = false;
1781                }
1782            }
1783            Ok(Event::Eof) => break,
1784            Err(e) => return Err(Box::new(e)),
1785            _ => {}
1786        }
1787        buf.clear();
1788    }
1789
1790    if !merge_cells.is_empty() {
1791        for cell_ref in &mut result {
1792            for merge_ref in &merge_cells {
1793                if cell_in_range(cell_ref, merge_ref) {
1794                    if let Some(top_left) = merge_ref.split(':').next() {
1795                        *cell_ref = top_left.to_string();
1796                    }
1797                }
1798            }
1799        }
1800    }
1801
1802    Ok(result)
1803}
1804
1805fn evaluate_cell(
1806    result: &mut Vec<String>,
1807    cell_ref: &str,
1808    cell_type: Option<&str>,
1809    current_val: &str,
1810    sst: Option<&crate::xml::shared_strings::XlsxSst>,
1811    value: &str,
1812    reg: bool,
1813    regex: Option<&regex::Regex>,
1814) {
1815    if cell_ref.is_empty() {
1816        return;
1817    }
1818    let val = match cell_type {
1819        Some("s") => {
1820            if let Ok(idx) = current_val.parse::<usize>() {
1821                sst.and_then(|s| s.si.get(idx))
1822                    .map(|si| extract_si_text(si))
1823                    .unwrap_or_default()
1824            } else {
1825                String::new()
1826            }
1827        }
1828        Some("inlineStr") => current_val.to_string(),
1829        Some("str") => current_val.to_string(),
1830        _ => current_val.to_string(),
1831    };
1832    let matched = if reg {
1833        regex.unwrap().is_match(&val)
1834    } else {
1835        val == value
1836    };
1837    if matched {
1838        result.push(cell_ref.to_string());
1839    }
1840}
1841
1842fn extract_si_text(si: &crate::xml::shared_strings::XlsxSi) -> String {
1843    if let Some(t) = &si.t {
1844        return t.val.clone();
1845    }
1846    si.r.iter()
1847        .filter_map(|r| r.t.as_ref().map(|t| t.val.clone()))
1848        .collect()
1849}
1850
1851fn cell_in_range(cell: &str, range: &str) -> bool {
1852    let parts: Vec<&str> = range.split(':').collect();
1853    if parts.len() != 2 {
1854        return false;
1855    }
1856    let (c, r) = cell_name_to_coordinates(cell).unwrap_or((0, 0));
1857    let (c1, r1) = cell_name_to_coordinates(parts[0]).unwrap_or((0, 0));
1858    let (c2, r2) = cell_name_to_coordinates(parts[1]).unwrap_or((0, 0));
1859    c >= c1 && c <= c2 && r >= r1 && r <= r2
1860}
1861
1862fn insert_page_break_ws(ws: &mut XlsxWorksheet, cell: &str) -> Result<()> {
1863    let (col, row) = cell_name_to_coordinates(cell).map_err(|e| e.to_string())?;
1864    let col = col - 1;
1865    let row = row - 1;
1866    if col == 0 && row == 0 {
1867        return Ok(());
1868    }
1869    if ws.row_breaks.is_none() {
1870        ws.row_breaks = Some(XlsxRowBreaks {
1871            breaks: XlsxBreaks::default(),
1872        });
1873    }
1874    if ws.col_breaks.is_none() {
1875        ws.col_breaks = Some(XlsxColBreaks {
1876            breaks: XlsxBreaks::default(),
1877        });
1878    }
1879    let row_breaks = ws.row_breaks.as_mut().unwrap();
1880    let col_breaks = ws.col_breaks.as_mut().unwrap();
1881    let mut row_brk = -1;
1882    let mut col_brk = -1;
1883    for (idx, brk) in row_breaks.breaks.brk.iter().enumerate() {
1884        if brk.id == Some(row as i64) {
1885            row_brk = idx as i32;
1886        }
1887    }
1888    for (idx, brk) in col_breaks.breaks.brk.iter().enumerate() {
1889        if brk.id == Some(col as i64) {
1890            col_brk = idx as i32;
1891        }
1892    }
1893    if row != 0 && row_brk == -1 {
1894        row_breaks.breaks.brk.push(XlsxBrk {
1895            id: Some(row as i64),
1896            max: Some((MAX_COLUMNS - 1) as i64),
1897            man: Some(true),
1898            ..Default::default()
1899        });
1900        row_breaks.breaks.manual_break_count =
1901            Some(row_breaks.breaks.manual_break_count.unwrap_or(0) + 1);
1902    }
1903    if col != 0 && col_brk == -1 {
1904        col_breaks.breaks.brk.push(XlsxBrk {
1905            id: Some(col as i64),
1906            max: Some((TOTAL_ROWS - 1) as i64),
1907            man: Some(true),
1908            ..Default::default()
1909        });
1910        col_breaks.breaks.manual_break_count =
1911            Some(col_breaks.breaks.manual_break_count.unwrap_or(0) + 1);
1912    }
1913    row_breaks.breaks.count = Some(row_breaks.breaks.brk.len() as i64);
1914    col_breaks.breaks.count = Some(col_breaks.breaks.brk.len() as i64);
1915    Ok(())
1916}
1917
1918fn remove_page_break_ws(ws: &mut XlsxWorksheet, cell: &str) -> Result<()> {
1919    let (col, row) = cell_name_to_coordinates(cell).map_err(|e| e.to_string())?;
1920    let col = col - 1;
1921    let row = row - 1;
1922    if col == 0 && row == 0 {
1923        return Ok(());
1924    }
1925    if ws.row_breaks.is_none() || ws.col_breaks.is_none() {
1926        return Ok(());
1927    }
1928    let row_breaks = ws.row_breaks.as_mut().unwrap();
1929    let col_breaks = ws.col_breaks.as_mut().unwrap();
1930    let row_brks = row_breaks.breaks.brk.len();
1931    let col_brks = col_breaks.breaks.brk.len();
1932    let mut remove_brk = |id: i64, brks: &mut Vec<XlsxBrk>| {
1933        let mut i = 0;
1934        while i < brks.len() {
1935            if brks[i].id == Some(id) {
1936                brks.remove(i);
1937            } else {
1938                i += 1;
1939            }
1940        }
1941    };
1942    if row_brks > 0 && row_brks == col_brks {
1943        remove_brk(row as i64, &mut row_breaks.breaks.brk);
1944        remove_brk(col as i64, &mut col_breaks.breaks.brk);
1945        row_breaks.breaks.count = Some(row_breaks.breaks.brk.len() as i64);
1946        col_breaks.breaks.count = Some(col_breaks.breaks.brk.len() as i64);
1947        row_breaks.breaks.manual_break_count =
1948            Some(row_breaks.breaks.manual_break_count.unwrap_or(1) - 1);
1949        col_breaks.breaks.manual_break_count =
1950            Some(col_breaks.breaks.manual_break_count.unwrap_or(1) - 1);
1951        return Ok(());
1952    }
1953    if row_brks > 0 && row_brks > col_brks {
1954        remove_brk(row as i64, &mut row_breaks.breaks.brk);
1955        row_breaks.breaks.count = Some(row_breaks.breaks.brk.len() as i64);
1956        row_breaks.breaks.manual_break_count =
1957            Some(row_breaks.breaks.manual_break_count.unwrap_or(1) - 1);
1958        return Ok(());
1959    }
1960    if col_brks > 0 && col_brks > row_brks {
1961        remove_brk(col as i64, &mut col_breaks.breaks.brk);
1962        col_breaks.breaks.count = Some(col_breaks.breaks.brk.len() as i64);
1963        col_breaks.breaks.manual_break_count =
1964            Some(col_breaks.breaks.manual_break_count.unwrap_or(1) - 1);
1965    }
1966    Ok(())
1967}
1968
1969fn get_sheet_state(visible: bool, very_hidden: Option<bool>) -> &'static str {
1970    if !visible && very_hidden == Some(true) {
1971        "veryHidden"
1972    } else {
1973        "hidden"
1974    }
1975}
1976
1977fn gen_sheet_passwd(plaintext: &str) -> String {
1978    let mut password: i64 = 0x0000;
1979    let mut char_pos: u32 = 1;
1980    for v in plaintext.chars() {
1981        let value = (v as i64) << char_pos;
1982        char_pos += 1;
1983        let rotated_bits = value >> 15;
1984        let value = value & 0x7fff;
1985        password ^= value | rotated_bits;
1986    }
1987    password ^= plaintext.chars().count() as i64;
1988    password ^= 0xCE4B;
1989    format!("{password:X}")
1990}
1991
1992fn image_extension_map() -> HashMap<String, String> {
1993    [
1994        (".bmp".to_string(), ".bmp".to_string()),
1995        (".emf".to_string(), ".emf".to_string()),
1996        (".emz".to_string(), ".emz".to_string()),
1997        (".gif".to_string(), ".gif".to_string()),
1998        (".ico".to_string(), ".ico".to_string()),
1999        (".jpg".to_string(), ".jpeg".to_string()),
2000        (".jpeg".to_string(), ".jpeg".to_string()),
2001        (".png".to_string(), ".png".to_string()),
2002        (".svg".to_string(), ".svg".to_string()),
2003        (".tif".to_string(), ".tiff".to_string()),
2004        (".tiff".to_string(), ".tiff".to_string()),
2005        (".wmf".to_string(), ".wmf".to_string()),
2006        (".wmz".to_string(), ".wmz".to_string()),
2007    ]
2008    .into_iter()
2009    .collect()
2010}
2011
2012pub(crate) fn set_content_type_part_image_extensions(file: &File) -> Result<()> {
2013    // Matches Go setContentTypePartImageExtensions: register image types by
2014    // file extension as Defaults in [Content_Types].xml.
2015    let image_types: HashMap<String, String> = [
2016        ("bmp".to_string(), "image/".to_string()),
2017        ("ico".to_string(), "image/x-".to_string()),
2018        ("jpeg".to_string(), "image/".to_string()),
2019        ("png".to_string(), "image/".to_string()),
2020        ("gif".to_string(), "image/".to_string()),
2021        ("svg".to_string(), "image/".to_string()),
2022        ("tiff".to_string(), "image/".to_string()),
2023        ("emf".to_string(), "image/x-".to_string()),
2024        ("wmf".to_string(), "image/x-".to_string()),
2025        ("emz".to_string(), "image/x-".to_string()),
2026        ("wmz".to_string(), "image/x-".to_string()),
2027    ]
2028    .into_iter()
2029    .collect();
2030    let mut ct = file.content_types_reader()?;
2031    for (extension, prefix) in image_types {
2032        let exists = ct.entries.iter().any(|e| {
2033            if let crate::xml::content_types::XlsxContentTypeEntry::Default(d) = e {
2034                d.extension == extension
2035            } else {
2036                false
2037            }
2038        });
2039        if !exists {
2040            let content_type = format!("{prefix}{extension}");
2041            ct.entries.push(XlsxContentTypeEntry::Default(XlsxDefault {
2042                extension: extension.clone(),
2043                content_type,
2044            }));
2045        }
2046    }
2047    *file.content_types.lock().unwrap() = Some(ct);
2048    Ok(())
2049}
2050
2051fn ignored_error_for_type(range_ref: &str, t: IgnoredErrorsType) -> XlsxIgnoredError {
2052    let base = XlsxIgnoredError {
2053        sqref: range_ref.to_string(),
2054        ..Default::default()
2055    };
2056    match t {
2057        IgnoredErrorsType::EvalError => XlsxIgnoredError {
2058            eval_error: Some(true),
2059            ..base
2060        },
2061        IgnoredErrorsType::TwoDigitTextYear => XlsxIgnoredError {
2062            two_digit_text_year: Some(true),
2063            ..base
2064        },
2065        IgnoredErrorsType::NumberStoredAsText => XlsxIgnoredError {
2066            number_stored_as_text: Some(true),
2067            ..base
2068        },
2069        IgnoredErrorsType::Formula => XlsxIgnoredError {
2070            formula: Some(true),
2071            ..base
2072        },
2073        IgnoredErrorsType::FormulaRange => XlsxIgnoredError {
2074            formula_range: Some(true),
2075            ..base
2076        },
2077        IgnoredErrorsType::UnlockedFormula => XlsxIgnoredError {
2078            unlocked_formula: Some(true),
2079            ..base
2080        },
2081        IgnoredErrorsType::EmptyCellReference => XlsxIgnoredError {
2082            empty_cell_reference: Some(true),
2083            ..base
2084        },
2085        IgnoredErrorsType::ListDataValidation => XlsxIgnoredError {
2086            list_data_validation: Some(true),
2087            ..base
2088        },
2089        IgnoredErrorsType::CalculatedColumn => XlsxIgnoredError {
2090            calculated_column: Some(true),
2091            ..base
2092        },
2093    }
2094}
2095
2096fn same_ignored_error_flags(a: &XlsxIgnoredError, b: &XlsxIgnoredError) -> bool {
2097    a.eval_error == b.eval_error
2098        && a.two_digit_text_year == b.two_digit_text_year
2099        && a.number_stored_as_text == b.number_stored_as_text
2100        && a.formula == b.formula
2101        && a.formula_range == b.formula_range
2102        && a.unlocked_formula == b.unlocked_formula
2103        && a.empty_cell_reference == b.empty_cell_reference
2104        && a.list_data_validation == b.list_data_validation
2105        && a.calculated_column == b.calculated_column
2106}
2107
2108fn adjust_range_sheet_name(data: &str, source: &str, target: &str) -> String {
2109    // Replace sheet name references in defined name formulas.
2110    let mut out = data.to_string();
2111    let patterns = vec![format!("{source}!"), format!("'{source}'!")];
2112    for p in patterns {
2113        out = out.replace(&p, &format!("{target}!"));
2114    }
2115    out
2116}
2117
2118pub(crate) fn ensure_shared_strings_rel(rels: &mut crate::xml::workbook::XlsxRelationships) {
2119    for rel in &rels.relationships {
2120        if rel.r#type == SOURCE_RELATIONSHIP_SHARED_STRINGS {
2121            return;
2122        }
2123    }
2124    let mut max_rid = 0i32;
2125    for rel in &rels.relationships {
2126        if let Ok(n) = rel.id.trim_start_matches("rId").parse::<i32>() {
2127            if n > max_rid {
2128                max_rid = n;
2129            }
2130        }
2131    }
2132    rels.relationships
2133        .push(crate::xml::workbook::XlsxRelationship {
2134            id: format!("rId{}", max_rid + 1),
2135            r#type: SOURCE_RELATIONSHIP_SHARED_STRINGS.to_string(),
2136            target: "sharedStrings.xml".to_string(),
2137            target_mode: None,
2138        });
2139}
2140
2141#[cfg(test)]
2142mod tests {
2143    use super::*;
2144
2145    #[test]
2146    fn new_sheet_round_trip() {
2147        let f = File::new_with_options(crate::options::Options::default());
2148        let idx = f.new_sheet("Sheet2").unwrap();
2149        assert_eq!(idx, 2);
2150        assert_eq!(f.get_sheet_name(idx).unwrap(), "Sheet2");
2151        assert_eq!(f.get_sheet_index("Sheet2").unwrap(), idx);
2152        assert!(f.get_sheet_list().contains(&"Sheet2".to_string()));
2153    }
2154
2155    #[test]
2156    fn sheet_name_and_map_round_trip() {
2157        let f = File::new_with_options(crate::options::Options::default());
2158        f.new_sheet("Sheet2").unwrap();
2159        f.set_sheet_name("Sheet2", "NewName").unwrap();
2160        assert_eq!(f.get_sheet_index("NewName").unwrap(), 2);
2161        let map = f.get_sheet_map();
2162        assert!(map.values().any(|n| n == "NewName"));
2163    }
2164
2165    #[test]
2166    fn sheet_visible_round_trip() {
2167        let f = File::new_with_options(crate::options::Options::default());
2168        f.new_sheet("Sheet2").unwrap();
2169        assert!(f.get_sheet_visible("Sheet2").unwrap());
2170        f.set_sheet_visible("Sheet2", false, None).unwrap();
2171        assert!(!f.get_sheet_visible("Sheet2").unwrap());
2172        f.set_sheet_visible("Sheet2", true, None).unwrap();
2173        assert!(f.get_sheet_visible("Sheet2").unwrap());
2174    }
2175
2176    #[test]
2177    fn panes_round_trip() {
2178        let f = File::new_with_options(crate::options::Options::default());
2179        let panes = Panes {
2180            freeze: true,
2181            x_split: 1,
2182            top_left_cell: "B1".to_string(),
2183            active_pane: "topRight".to_string(),
2184            selection: vec![Selection {
2185                sqref: "K16".to_string(),
2186                active_cell: "K16".to_string(),
2187                pane: "topRight".to_string(),
2188            }],
2189            ..Default::default()
2190        };
2191        f.set_panes("Sheet1", &panes).unwrap();
2192        let got = f.get_panes("Sheet1").unwrap();
2193        assert_eq!(got.freeze, true);
2194        assert_eq!(got.x_split, 1);
2195        assert_eq!(got.top_left_cell, "B1");
2196    }
2197
2198    #[test]
2199    fn sheet_protection_round_trip() {
2200        let f = File::new_with_options(crate::options::Options::default());
2201        let opts = SheetProtectionOptions {
2202            password: "password".to_string(),
2203            select_locked_cells: true,
2204            ..Default::default()
2205        };
2206        f.protect_sheet("Sheet1", &opts).unwrap();
2207        let got = f.get_sheet_protection("Sheet1").unwrap();
2208        assert!(got.select_locked_cells);
2209        f.unprotect_sheet("Sheet1", Some("password")).unwrap();
2210        let got = f.get_sheet_protection("Sheet1").unwrap();
2211        assert!(!got.select_locked_cells);
2212    }
2213
2214    #[test]
2215    fn page_break_round_trip() {
2216        let f = File::new_with_options(crate::options::Options::default());
2217        f.insert_page_break("Sheet1", "B2").unwrap();
2218        let ws = f.work_sheet_reader("Sheet1").unwrap();
2219        assert!(ws.row_breaks.is_some());
2220        assert!(ws.col_breaks.is_some());
2221        f.remove_page_break("Sheet1", "B2").unwrap();
2222        let ws = f.work_sheet_reader("Sheet1").unwrap();
2223        assert!(ws.row_breaks.as_ref().unwrap().breaks.brk.is_empty());
2224    }
2225
2226    #[test]
2227    fn sheet_dimension_round_trip() {
2228        let f = File::new_with_options(crate::options::Options::default());
2229        f.set_sheet_dimension("Sheet1", "A1:D5").unwrap();
2230        assert_eq!(f.get_sheet_dimension("Sheet1").unwrap(), "A1:D5");
2231        f.set_sheet_dimension("Sheet1", "").unwrap();
2232        assert!(f.get_sheet_dimension("Sheet1").unwrap().is_empty());
2233    }
2234
2235    #[test]
2236    fn search_sheet_basic() {
2237        let f = File::new_with_options(crate::options::Options::default());
2238        f.set_cell_str("Sheet1", "A1", "hello").unwrap();
2239        f.set_cell_str("Sheet1", "B2", "world").unwrap();
2240        let refs = f.search_sheet("Sheet1", "hello", None).unwrap();
2241        assert!(refs.contains(&"A1".to_string()));
2242    }
2243
2244    #[test]
2245    fn search_sheet_shared_string() {
2246        let f = File::new_with_options(crate::options::Options::default());
2247        f.set_cell_str("Sheet1", "A1", "shared_text").unwrap();
2248        let refs = f.search_sheet("Sheet1", "shared_text", None).unwrap();
2249        assert_eq!(refs, vec!["A1".to_string()]);
2250    }
2251
2252    #[test]
2253    fn search_sheet_regex() {
2254        let f = File::new_with_options(crate::options::Options::default());
2255        f.set_cell_str("Sheet1", "A1", "hello world").unwrap();
2256        f.set_cell_str("Sheet1", "B2", "hello rust").unwrap();
2257        let refs = f.search_sheet("Sheet1", r"hello \w+", Some(true)).unwrap();
2258        assert_eq!(refs.len(), 2);
2259        assert!(refs.contains(&"A1".to_string()));
2260        assert!(refs.contains(&"B2".to_string()));
2261    }
2262
2263    #[test]
2264    fn search_sheet_inline_string() {
2265        let f = File::new_with_options(crate::options::Options::default());
2266        f.set_cell_value(
2267            "Sheet1",
2268            "A1",
2269            vec![crate::xml::common::RichTextRun {
2270                text: "inline".to_string(),
2271                ..Default::default()
2272            }],
2273        )
2274        .unwrap();
2275        let refs = f.search_sheet("Sheet1", "inline", None).unwrap();
2276        assert_eq!(refs, vec!["A1".to_string()]);
2277    }
2278
2279    #[test]
2280    fn search_sheet_rich_text_shared_string() {
2281        let f = File::new_with_options(crate::options::Options::default());
2282        let runs = vec![
2283            crate::xml::common::RichTextRun {
2284                text: "bold".to_string(),
2285                ..Default::default()
2286            },
2287            crate::xml::common::RichTextRun {
2288                text: "text".to_string(),
2289                ..Default::default()
2290            },
2291        ];
2292        f.set_cell_rich_text("Sheet1", "A1", runs).unwrap();
2293        let refs = f.search_sheet("Sheet1", "boldtext", None).unwrap();
2294        assert_eq!(refs, vec!["A1".to_string()]);
2295    }
2296
2297    #[test]
2298    fn search_sheet_merge_cells() {
2299        let f = File::new_with_options(crate::options::Options::default());
2300        f.set_cell_str("Sheet1", "A1", "merged").unwrap();
2301        f.merge_cell("Sheet1", "A1", "B2").unwrap();
2302        let refs = f.search_sheet("Sheet1", "merged", None).unwrap();
2303        assert_eq!(refs, vec!["A1".to_string()]);
2304    }
2305
2306    #[test]
2307    fn search_sheet_after_save_and_reopen() {
2308        let f = File::new_with_options(crate::options::Options::default());
2309        f.set_cell_str("Sheet1", "A1", "persisted").unwrap();
2310        let buf = f.write_to_buffer().unwrap();
2311        let f2 = File::open_reader(
2312            std::io::Cursor::new(buf),
2313            0,
2314            crate::options::Options::default(),
2315        )
2316        .unwrap();
2317        let refs = f2.search_sheet("Sheet1", "persisted", None).unwrap();
2318        assert_eq!(refs, vec!["A1".to_string()]);
2319    }
2320
2321    #[test]
2322    fn ignored_errors_round_trip() {
2323        let f = File::new_with_options(crate::options::Options::default());
2324        f.add_ignored_errors("Sheet1", "A1:A2", IgnoredErrorsType::NumberStoredAsText)
2325            .unwrap();
2326        let ws = f.work_sheet_reader("Sheet1").unwrap();
2327        let errors = ws.ignored_errors.as_ref().unwrap();
2328        assert_eq!(errors.ignored_error.len(), 1);
2329        assert!(errors.ignored_error[0].number_stored_as_text.unwrap());
2330    }
2331
2332    #[test]
2333    fn move_and_copy_sheet() {
2334        let f = File::new_with_options(crate::options::Options::default());
2335        f.new_sheet("Sheet2").unwrap();
2336        f.new_sheet("Sheet3").unwrap();
2337        f.move_sheet("Sheet3", "Sheet1").unwrap();
2338        let list = f.get_sheet_list();
2339        assert_eq!(list[0], "Sheet3");
2340        f.copy_sheet(1, 2).unwrap();
2341    }
2342
2343    #[test]
2344    fn sheet_background_from_bytes() {
2345        let f = File::new_with_options(crate::options::Options::default());
2346        // 1x1 red PNG
2347        let png = vec![
2348            0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48,
2349            0x44, 0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x02, 0x00, 0x00,
2350            0x00, 0x90, 0x77, 0x53, 0xde, 0x00, 0x00, 0x00, 0x0c, 0x49, 0x44, 0x41, 0x54, 0x08,
2351            0x99, 0x63, 0xf8, 0x0f, 0x00, 0x00, 0x01, 0x01, 0x00, 0x05, 0x18, 0xd8, 0x4e, 0x00,
2352            0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82,
2353        ];
2354        f.set_sheet_background_from_bytes("Sheet1", "png", &png)
2355            .unwrap();
2356        let ws = f.work_sheet_reader("Sheet1").unwrap();
2357        assert!(ws.picture.is_some());
2358    }
2359
2360    #[test]
2361    fn sheet_props_round_trip() {
2362        let f = File::new_with_options(crate::options::Options::default());
2363        let expected = SheetPropsOptions {
2364            code_name: Some("code".to_string()),
2365            enable_format_conditions_calculation: Some(true),
2366            published: Some(true),
2367            auto_page_breaks: Some(true),
2368            fit_to_page: Some(true),
2369            tab_color_indexed: Some(1),
2370            tab_color_rgb: Some("FFFF00".to_string()),
2371            tab_color_theme: Some(1),
2372            tab_color_tint: Some(1.0),
2373            outline_pr_summary_below: Some(true),
2374            outline_pr_summary_right: Some(true),
2375            base_col_width: Some(8),
2376            default_col_width: Some(10.0),
2377            default_row_height: Some(10.0),
2378            custom_height: Some(true),
2379            zero_height: Some(true),
2380            thick_top: Some(true),
2381            thick_bottom: Some(true),
2382            ..Default::default()
2383        };
2384        f.set_sheet_props("Sheet1", &expected).unwrap();
2385        let got = f.get_sheet_props("Sheet1").unwrap();
2386        assert_eq!(expected, got);
2387    }
2388
2389    #[test]
2390    fn page_margins_round_trip() {
2391        let f = File::new_with_options(crate::options::Options::default());
2392        let expected = PageMarginsOptions {
2393            left: Some(1.0),
2394            right: Some(1.0),
2395            top: Some(1.0),
2396            bottom: Some(1.0),
2397            header: Some(1.0),
2398            footer: Some(1.0),
2399            horizontally: Some(true),
2400            vertically: Some(true),
2401        };
2402        f.set_page_margins("Sheet1", &expected).unwrap();
2403        let got = f.get_page_margins("Sheet1").unwrap();
2404        assert_eq!(expected, got);
2405    }
2406
2407    #[test]
2408    fn sheet_view_round_trip() {
2409        let f = File::new_with_options(crate::options::Options::default());
2410        let expected = SheetViewOptions {
2411            default_grid_color: Some(false),
2412            right_to_left: Some(false),
2413            show_formulas: Some(false),
2414            show_grid_lines: Some(false),
2415            show_row_col_headers: Some(false),
2416            show_ruler: Some(false),
2417            show_zeros: Some(false),
2418            top_left_cell: Some("A1".to_string()),
2419            view: Some("normal".to_string()),
2420            zoom_scale: Some(120.0),
2421            // The new file template has tabSelected="1", so this stays true.
2422            tab_selected: Some(true),
2423            // Value-type fields that are not present in the XML default to false.
2424            show_outline_symbols: Some(false),
2425            show_white_space: Some(false),
2426            window_protection: Some(false),
2427        };
2428        f.set_sheet_view("Sheet1", 0, &expected).unwrap();
2429        let got = f.get_sheet_view("Sheet1", 0).unwrap();
2430        assert_eq!(expected, got);
2431
2432        // Negative index points at the same single view.
2433        f.set_sheet_view("Sheet1", -1, &expected).unwrap();
2434        let got = f.get_sheet_view("Sheet1", -1).unwrap();
2435        assert_eq!(expected, got);
2436
2437        // Invalid view values are ignored.
2438        let invalid_view = SheetViewOptions {
2439            view: Some("invalid".to_string()),
2440            ..Default::default()
2441        };
2442        f.set_sheet_view("Sheet1", 0, &invalid_view).unwrap();
2443        let got = f.get_sheet_view("Sheet1", 0).unwrap();
2444        assert_eq!(Some("normal".to_string()), got.view);
2445
2446        // Invalid zoom values are ignored.
2447        let invalid_zoom = SheetViewOptions {
2448            zoom_scale: Some(5.0),
2449            ..Default::default()
2450        };
2451        f.set_sheet_view("Sheet1", 0, &invalid_zoom).unwrap();
2452        let got = f.get_sheet_view("Sheet1", 0).unwrap();
2453        assert_eq!(Some(120.0), got.zoom_scale);
2454
2455        // Out-of-range indices return an error.
2456        let empty = SheetViewOptions::default();
2457        assert_eq!(
2458            "view index 1 out of range",
2459            f.set_sheet_view("Sheet1", 1, &empty)
2460                .unwrap_err()
2461                .to_string()
2462        );
2463        assert_eq!(
2464            "view index -2 out of range",
2465            f.set_sheet_view("Sheet1", -2, &empty)
2466                .unwrap_err()
2467                .to_string()
2468        );
2469        assert!(f.get_sheet_view("Sheet1", 1).is_err());
2470        assert!(f.get_sheet_view("Sheet1", -2).is_err());
2471
2472        // Non-existent worksheet returns an error.
2473        assert!(f.set_sheet_view("SheetN", 0, &empty).is_err());
2474        assert!(f.get_sheet_view("SheetN", 0).is_err());
2475    }
2476
2477    #[test]
2478    fn sheet_view_page_break_preview() {
2479        let f = File::new_with_options(crate::options::Options::default());
2480        let opts = SheetViewOptions {
2481            view: Some("pageBreakPreview".to_string()),
2482            zoom_scale: Some(200.0),
2483            ..Default::default()
2484        };
2485        f.set_sheet_view("Sheet1", 0, &opts).unwrap();
2486        let got = f.get_sheet_view("Sheet1", 0).unwrap();
2487        assert_eq!(Some("pageBreakPreview".to_string()), got.view);
2488        assert_eq!(Some(200.0), got.zoom_scale);
2489    }
2490
2491    #[test]
2492    fn sheet_props_defaults() {
2493        let f = File::new_with_options(crate::options::Options::default());
2494        let opts = f.get_sheet_props("Sheet1").unwrap();
2495        assert_eq!(Some(true), opts.enable_format_conditions_calculation);
2496        assert_eq!(Some(true), opts.published);
2497        assert_eq!(Some(true), opts.auto_page_breaks);
2498        assert_eq!(Some(true), opts.outline_pr_summary_below);
2499        assert_eq!(Some(8), opts.base_col_width);
2500        assert_eq!(Some(15.0), opts.default_row_height);
2501    }
2502
2503    #[test]
2504    fn page_margins_defaults() {
2505        let f = File::new_with_options(crate::options::Options::default());
2506        let opts = f.get_page_margins("Sheet1").unwrap();
2507        assert_eq!(Some(0.7), opts.left);
2508        assert_eq!(Some(0.7), opts.right);
2509        assert_eq!(Some(0.75), opts.top);
2510        assert_eq!(Some(0.75), opts.bottom);
2511        assert_eq!(Some(0.3), opts.header);
2512        assert_eq!(Some(0.3), opts.footer);
2513    }
2514}