1use std::collections::HashMap;
6
7use crate::constants::{
8 DEFAULT_COL_WIDTH, DEFAULT_COL_WIDTH_PIXELS, DEFAULT_FONT_SIZE, MAX_COLUMN_WIDTH, TOTAL_ROWS,
9};
10use crate::errors::Result;
11use crate::errors::{ErrColumnNumber, ErrColumnWidth, ErrMaxRows, ErrSheetNotExist};
12use crate::file::File;
13use crate::lib_util::{cell_name_to_coordinates, column_name_to_number};
14use crate::options::Options;
15use crate::row::cell_value_string;
16use crate::styles::Font;
17use crate::xml::common::RichTextRun;
18use crate::xml::worksheet::{XlsxCol, XlsxCols, XlsxWorksheet};
19
20impl File {
21 pub fn set_col_width(
23 &self,
24 sheet: &str,
25 start_col: &str,
26 end_col: &str,
27 width: f64,
28 ) -> Result<()> {
29 if width > MAX_COLUMN_WIDTH as f64 {
30 return Err(Box::new(ErrColumnWidth));
31 }
32 let start = column_name_to_number(start_col)?;
33 let end = column_name_to_number(end_col)?;
34 if start > end {
35 return Err(ErrColumnNumber.to_string().into());
36 }
37 let path = self
38 .get_sheet_xml_path(sheet)
39 .ok_or_else(|| ErrSheetNotExist {
40 sheet_name: sheet.to_string(),
41 })?;
42 let mut ws = self.work_sheet_reader(sheet)?;
43 ensure_cols(&mut ws);
44 let cols = ws.cols.as_mut().unwrap();
45 merge_col_range(&mut cols.col, start as i64, end as i64, |col| {
46 col.width = Some(width);
47 col.custom_width = Some(true);
48 col.best_fit = Some(true);
49 });
50 self.sheet.insert(path, ws);
51 Ok(())
52 }
53
54 pub fn get_col_width(&self, sheet: &str, col: &str) -> Result<f64> {
56 let col_num = column_name_to_number(col)?;
57 let ws = self.work_sheet_reader(sheet)?;
58 if let Some(cols) = &ws.cols {
59 for c in &cols.col {
60 if col_num as i64 >= c.min && col_num as i64 <= c.max {
61 return Ok(c.width.unwrap_or(DEFAULT_COL_WIDTH));
62 }
63 }
64 }
65 if let Some(fmt) = &ws.sheet_format_pr {
66 if let Some(w) = fmt.default_col_width {
67 return Ok(w);
68 }
69 }
70 Ok(DEFAULT_COL_WIDTH)
71 }
72
73 pub fn set_col_visible(
75 &self,
76 sheet: &str,
77 start_col: &str,
78 end_col: &str,
79 visible: bool,
80 ) -> Result<()> {
81 let start = column_name_to_number(start_col)?;
82 let end = column_name_to_number(end_col)?;
83 let path = self
84 .get_sheet_xml_path(sheet)
85 .ok_or_else(|| ErrSheetNotExist {
86 sheet_name: sheet.to_string(),
87 })?;
88 let mut ws = self.work_sheet_reader(sheet)?;
89 ensure_cols(&mut ws);
90 let cols = ws.cols.as_mut().unwrap();
91 merge_col_range(&mut cols.col, start as i64, end as i64, |col| {
92 col.hidden = Some(!visible);
93 });
94 self.sheet.insert(path, ws);
95 Ok(())
96 }
97
98 pub fn get_col_visible(&self, sheet: &str, col: &str) -> Result<bool> {
100 let col_num = column_name_to_number(col)?;
101 let ws = self.work_sheet_reader(sheet)?;
102 if let Some(cols) = &ws.cols {
103 for c in &cols.col {
104 if col_num as i64 >= c.min && col_num as i64 <= c.max {
105 return Ok(!c.hidden.unwrap_or(false));
106 }
107 }
108 }
109 Ok(true)
110 }
111
112 pub fn set_col_outline_level(
114 &self,
115 sheet: &str,
116 start_col: &str,
117 end_col: &str,
118 level: u8,
119 ) -> Result<()> {
120 if level == 0 || level > 7 {
121 return Err(Box::new(crate::errors::ErrOutlineLevel));
122 }
123 let start = column_name_to_number(start_col)?;
124 let end = column_name_to_number(end_col)?;
125 let path = self
126 .get_sheet_xml_path(sheet)
127 .ok_or_else(|| ErrSheetNotExist {
128 sheet_name: sheet.to_string(),
129 })?;
130 let mut ws = self.work_sheet_reader(sheet)?;
131 ensure_cols(&mut ws);
132 let cols = ws.cols.as_mut().unwrap();
133 merge_col_range(&mut cols.col, start as i64, end as i64, |col| {
134 col.outline_level = Some(level);
135 });
136 self.sheet.insert(path, ws);
137 Ok(())
138 }
139
140 pub fn get_col_outline_level(&self, sheet: &str, col: &str) -> Result<u8> {
142 let col_num = column_name_to_number(col)?;
143 let ws = self.work_sheet_reader(sheet)?;
144 if let Some(cols) = &ws.cols {
145 for c in &cols.col {
146 if col_num as i64 >= c.min && col_num as i64 <= c.max {
147 return Ok(c.outline_level.unwrap_or(0));
148 }
149 }
150 }
151 Ok(0)
152 }
153
154 pub fn insert_cols(&self, sheet: &str, col: &str, n: usize) -> Result<()> {
156 if n == 0 {
157 return Ok(());
158 }
159 let col_num = column_name_to_number(col)?;
160 let path = self
161 .get_sheet_xml_path(sheet)
162 .ok_or_else(|| ErrSheetNotExist {
163 sheet_name: sheet.to_string(),
164 })?;
165 let mut ws = self.work_sheet_reader(sheet)?;
166 shift_cols_right(&mut ws, col_num as i64, n as i64);
167 self.sheet.insert(path, ws);
168 Ok(())
169 }
170
171 pub fn remove_col(&self, sheet: &str, col: &str) -> Result<()> {
173 let col_num = column_name_to_number(col)?;
174 let path = self
175 .get_sheet_xml_path(sheet)
176 .ok_or_else(|| ErrSheetNotExist {
177 sheet_name: sheet.to_string(),
178 })?;
179 let mut ws = self.work_sheet_reader(sheet)?;
180 for row in &mut ws.sheet_data.row {
182 row.c.retain(|c| {
183 if let Some(name) = &c.r {
184 if let Ok((cnum, _)) = crate::lib_util::cell_name_to_coordinates(name) {
185 return cnum != col_num;
186 }
187 }
188 true
189 });
190 for cell in &mut row.c {
191 if let Some(name) = &cell.r {
192 if let Ok((cnum, row_num)) = crate::lib_util::cell_name_to_coordinates(name) {
193 if cnum > col_num {
194 if let Ok(new_name) =
195 crate::lib_util::coordinates_to_cell_name(cnum - 1, row_num, false)
196 {
197 cell.r = Some(new_name);
198 }
199 }
200 }
201 }
202 }
203 }
204 if let Some(cols) = ws.cols.as_mut() {
206 let mut new_cols = Vec::new();
207 for c in &cols.col {
208 if c.max < col_num as i64 {
209 new_cols.push(c.clone());
210 } else if c.min > col_num as i64 {
211 let mut nc = c.clone();
212 nc.min -= 1;
213 nc.max -= 1;
214 new_cols.push(nc);
215 } else {
216 if c.min < col_num as i64 {
217 let mut before = c.clone();
218 before.max = col_num as i64 - 1;
219 new_cols.push(before);
220 }
221 if c.max > col_num as i64 {
222 let mut after = c.clone();
223 after.min = col_num as i64;
224 after.max -= 1;
225 new_cols.push(after);
226 }
227 }
228 }
229 cols.col = new_cols;
230 }
231 self.sheet.insert(path, ws);
232 Ok(())
233 }
234
235 pub fn set_col_style(
237 &self,
238 sheet: &str,
239 start_col: &str,
240 end_col: &str,
241 style_id: i32,
242 ) -> Result<()> {
243 if style_id < 0 {
244 return Err(crate::errors::new_invalid_style_id_error(style_id).into());
245 }
246 let style_count = self
247 .styles_reader()?
248 .cell_xfs
249 .as_ref()
250 .map(|x| x.xf.len())
251 .unwrap_or(1) as i32;
252 if style_id >= style_count {
253 return Err(crate::errors::new_invalid_style_id_error(style_id).into());
254 }
255 let start = column_name_to_number(start_col)?;
256 let end = column_name_to_number(end_col)?;
257 let path = self
258 .get_sheet_xml_path(sheet)
259 .ok_or_else(|| ErrSheetNotExist {
260 sheet_name: sheet.to_string(),
261 })?;
262 let mut ws = self.work_sheet_reader(sheet)?;
263 ensure_cols(&mut ws);
264 let cols = ws.cols.as_mut().unwrap();
265 merge_col_range(&mut cols.col, start as i64, end as i64, |col| {
266 col.style = Some(style_id as i64);
267 });
268 self.sheet.insert(path, ws);
269 Ok(())
270 }
271
272 pub fn get_col_style(&self, sheet: &str, col: &str) -> Result<i32> {
274 let col_num = column_name_to_number(col)?;
275 let ws = self.work_sheet_reader(sheet)?;
276 if let Some(cols) = &ws.cols {
277 for c in &cols.col {
278 if col_num as i64 >= c.min && col_num as i64 <= c.max {
279 return Ok(c.style.unwrap_or(0) as i32);
280 }
281 }
282 }
283 Ok(0)
284 }
285
286 pub fn auto_fit_col_width(&self, sheet: &str, columns: &str) -> Result<()> {
291 let (min_col, max_col) = parse_col_range(columns)?;
292 let rows = self.get_rows(sheet, Options::default())?;
293 let default_fnt = read_default_font(self);
294 let path = self
295 .get_sheet_xml_path(sheet)
296 .ok_or_else(|| ErrSheetNotExist {
297 sheet_name: sheet.to_string(),
298 })?;
299 let mut ws = self.work_sheet_reader(sheet)?;
300 for col in min_col..=max_col {
301 let mut width = 0.0;
302 for (row_idx, _) in rows.iter().enumerate() {
303 let cell =
304 crate::lib_util::coordinates_to_cell_name(col, row_idx as i32 + 1, false)?;
305 let val = match self.calc_cell_value(sheet, &cell) {
306 Ok(v) => v,
307 Err(_) => continue,
308 };
309 if val.is_empty() {
310 continue;
311 }
312 let style_id = self.get_cell_style(sheet, &cell).unwrap_or(0);
313 let mut fnt = default_fnt.clone();
314 if let Ok(style) = self.get_style(style_id) {
315 if let Some(font) = style.font {
316 fnt = font;
317 }
318 }
319 if let Ok(cell_type) = self.get_cell_type(sheet, &cell) {
320 if cell_type == crate::cell::CellType::InlineString
321 || cell_type == crate::cell::CellType::SharedString
322 {
323 let runs = self.get_cell_rich_text(sheet, &cell).unwrap_or_default();
324 let w = calc_rich_text_width(&fnt, &runs);
325 if w > width {
326 width = w;
327 }
328 continue;
329 }
330 }
331 let w = calc_text_width(&fnt, &val);
332 if w > width {
333 width = w;
334 }
335 }
336 if width > 0.0 {
337 width += 2.0;
338 if width > MAX_COLUMN_WIDTH as f64 {
339 width = MAX_COLUMN_WIDTH as f64;
340 }
341 ensure_cols(&mut ws);
342 let cols = ws.cols.as_mut().unwrap();
343 merge_col_range(&mut cols.col, col as i64, col as i64, |c| {
344 c.hidden = Some(false);
345 c.width = Some(width);
346 c.custom_width = Some(true);
347 });
348 }
349 }
350 self.sheet.insert(path, ws);
351 Ok(())
352 }
353}
354
355#[derive(Debug)]
361pub struct Cols<'a> {
362 file: &'a File,
363 ws: XlsxWorksheet,
364 cur_col: i32,
365 total_cols: i32,
366 total_rows: i32,
367 err: Option<String>,
368}
369
370impl<'a> Cols<'a> {
371 pub fn next(&mut self) -> bool {
373 self.cur_col += 1;
374 self.cur_col <= self.total_cols
375 }
376
377 pub fn error(&self) -> Option<&str> {
379 self.err.as_deref()
380 }
381
382 pub fn rows(&self, opts: Options) -> Result<Vec<String>> {
384 if self.cur_col < 1 || self.cur_col > self.total_cols || self.total_rows == 0 {
385 return Ok(Vec::new());
386 }
387 let mut cells = vec![String::new(); self.total_rows as usize];
388 for row in &self.ws.sheet_data.row {
389 let row_num = row.r.unwrap_or(0) as i32;
390 if row_num < 1 || row_num > self.total_rows {
391 continue;
392 }
393 let mut cell_col: i32 = 0;
394 for c in &row.c {
395 cell_col += 1;
396 if let Some(name) = &c.r {
397 cell_col = cell_name_to_coordinates(name)?.0;
398 }
399 if cell_col == self.cur_col {
400 cells[(row_num - 1) as usize] =
401 cell_value_string(self.file, c, opts.raw_cell_value);
402 }
403 }
404 }
405 Ok(cells)
406 }
407}
408
409impl File {
410 pub fn cols(&self, sheet: &str) -> Result<Cols<'_>> {
412 let ws = self.work_sheet_reader(sheet)?;
413 let (total_rows, total_cols) = scan_dimensions(&ws)?;
414 Ok(Cols {
415 file: self,
416 ws,
417 cur_col: 0,
418 total_cols,
419 total_rows,
420 err: None,
421 })
422 }
423
424 pub fn get_cols(&self, sheet: &str, opts: Options) -> Result<Vec<Vec<String>>> {
426 let mut cols = self.cols(sheet)?;
427 let mut results = Vec::new();
428 while cols.next() {
429 results.push(cols.rows(opts.clone())?);
430 }
431 Ok(results)
432 }
433}
434
435fn scan_dimensions(ws: &XlsxWorksheet) -> Result<(i32, i32)> {
436 let mut total_rows = 0;
437 let mut total_cols = 0;
438 let mut row_num = 0;
439 for row in &ws.sheet_data.row {
440 row_num += 1;
441 if let Some(r) = row.r {
442 row_num = r as i32;
443 }
444 if row_num > TOTAL_ROWS {
445 return Err(Box::new(ErrMaxRows));
446 }
447 total_rows = total_rows.max(row_num);
448 let mut cell_col = 0;
449 for c in &row.c {
450 cell_col += 1;
451 if let Some(name) = &c.r {
452 cell_col = cell_name_to_coordinates(name)?.0;
453 }
454 total_cols = total_cols.max(cell_col);
455 }
456 }
457 Ok((total_rows, total_cols))
458}
459
460fn ensure_cols(ws: &mut crate::xml::worksheet::XlsxWorksheet) {
465 if ws.cols.is_none() {
466 ws.cols = Some(XlsxCols::default());
467 }
468}
469
470fn merge_col_range(cols: &mut Vec<XlsxCol>, min: i64, max: i64, mut f: impl FnMut(&mut XlsxCol)) {
473 let mut new_cols = Vec::new();
474 let mut covered = false;
475 for col in cols.drain(..) {
476 if col.max < min || col.min > max {
477 new_cols.push(col);
478 continue;
479 }
480 covered = true;
481 if col.min < min {
482 let mut before = col.clone();
483 before.max = min - 1;
484 new_cols.push(before);
485 }
486 let mut target = col.clone();
487 target.min = target.min.max(min);
488 target.max = target.max.min(max);
489 f(&mut target);
490 new_cols.push(target);
491 if col.max > max {
492 let mut after = col;
493 after.min = max + 1;
494 new_cols.push(after);
495 }
496 }
497 if !covered {
498 let mut target = XlsxCol {
499 min,
500 max,
501 ..Default::default()
502 };
503 f(&mut target);
504 new_cols.push(target);
505 }
506 new_cols.sort_by_key(|c| c.min);
508 let mut merged: Vec<XlsxCol> = Vec::new();
509 for col in new_cols {
510 if let Some(last) = merged.last_mut() {
511 if last.width == col.width
512 && last.hidden == col.hidden
513 && last.outline_level == col.outline_level
514 && last.style == col.style
515 && last.custom_width == col.custom_width
516 && last.best_fit == col.best_fit
517 && last.max + 1 == col.min
518 {
519 last.max = col.max;
520 continue;
521 }
522 }
523 merged.push(col);
524 }
525 *cols = merged;
526}
527
528fn shift_cols_right(ws: &mut crate::xml::worksheet::XlsxWorksheet, start_col: i64, n: i64) {
529 for row in &mut ws.sheet_data.row {
531 for cell in &mut row.c {
532 if let Some(name) = &cell.r {
533 if let Ok((col, row_num)) = crate::lib_util::cell_name_to_coordinates(name) {
534 if col >= start_col as i32 {
535 let new_col = (col as i64 + n) as i32;
536 if let Ok(new_name) =
537 crate::lib_util::coordinates_to_cell_name(new_col, row_num, false)
538 {
539 cell.r = Some(new_name);
540 }
541 }
542 }
543 }
544 }
545 }
546 if let Some(cols) = ws.cols.as_mut() {
548 for col in &mut cols.col {
549 if col.min >= start_col {
550 col.min += n;
551 col.max += n;
552 } else if col.max >= start_col {
553 col.max += n;
554 }
555 }
556 }
557}
558
559fn parse_col_range(columns: &str) -> Result<(i32, i32)> {
560 let parts: Vec<&str> = columns.split(':').collect();
561 let min = column_name_to_number(parts[0])?;
562 let max = if parts.len() == 2 {
563 column_name_to_number(parts[1])?
564 } else {
565 min
566 };
567 if min <= max {
568 Ok((min, max))
569 } else {
570 Ok((max, min))
571 }
572}
573
574fn read_default_font(file: &File) -> Font {
575 if let Ok(styles) = file.styles_reader() {
576 if let Some(fonts) = styles.fonts {
577 if let Some(f) = fonts.font.first() {
578 return Font {
579 size: f.sz.as_ref().and_then(|s| s.val),
580 name: f.name.as_ref().and_then(|n| n.val.clone()),
581 family: f.family.as_ref().and_then(|v| v.val),
582 bold: f.b.as_ref().and_then(|b| b.val),
583 italic: f.i.as_ref().and_then(|i| i.val),
584 strike: f.strike.as_ref().and_then(|s| s.val),
585 ..Default::default()
586 };
587 }
588 }
589 }
590 Font {
591 size: Some(DEFAULT_FONT_SIZE),
592 name: Some("Calibri".to_string()),
593 ..Default::default()
594 }
595}
596
597fn font_width_factors(name: Option<&str>) -> (f64, f64, f64) {
598 let map: HashMap<String, (f64, f64, f64)> = [
599 ("calibri", (0.97, 1.30, 1.00)),
600 ("aptos", (1.03, 1.37, 1.00)),
601 ("arial", (1.07, 1.42, 1.00)),
602 ("arial narrow", (0.88, 1.20, 1.00)),
603 ("calibri light", (0.98, 1.19, 1.00)),
604 ("cambria", (1.07, 1.28, 1.00)),
605 ("consolas", (1.21, 1.21, 1.00)),
606 ("courier new", (1.45, 1.45, 1.00)),
607 ("times new roman", (1.07, 1.42, 1.00)),
608 ("verdana", (1.10, 1.45, 1.00)),
609 ]
610 .iter()
611 .map(|(k, v)| (k.to_string(), *v))
612 .collect();
613 name.and_then(|n| map.get(&n.to_lowercase()))
614 .copied()
615 .unwrap_or((1.0, 1.0, 1.0))
616}
617
618fn is_wide_rune(r: char) -> bool {
619 matches!(
620 r,
621 '\u{1100}'..='\u{115F}'
622 | '\u{2E80}'..='\u{9FFF}'
623 | '\u{A960}'..='\u{A97F}'
624 | '\u{AC00}'..='\u{D7AF}'
625 | '\u{F900}'..='\u{FAFF}'
626 | '\u{FF01}'..='\u{FF60}'
627 | '\u{FFE0}'..='\u{FFE6}'
628 )
629}
630
631fn calc_text_width(font: &Font, text: &str) -> f64 {
632 let mut lower = 0.0;
633 let mut upper = 0.0;
634 let mut wide = 0.0;
635 for r in text.chars() {
636 if r == '\n' || r == '\r' {
637 continue;
638 }
639 if is_wide_rune(r) {
640 wide += 2.0;
641 } else if r.is_uppercase() {
642 upper += 1.0;
643 } else {
644 lower += 1.0;
645 }
646 }
647 let size = font.size.unwrap_or(DEFAULT_FONT_SIZE);
648 let (lf, uf, wf) = font_width_factors(font.name.as_deref());
649 let mut w = lower * lf + upper * uf + wide * wf;
650 w *= size / DEFAULT_FONT_SIZE;
651 if font.bold.unwrap_or(false) {
652 w *= 1.05;
653 }
654 if font.italic.unwrap_or(false) {
655 w *= 1.05;
656 }
657 if font.vert_align.as_deref().unwrap_or("baseline") != "baseline" {
658 w *= 0.6;
659 }
660 w
661}
662
663fn calc_rich_text_width(default_font: &Font, runs: &[RichTextRun]) -> f64 {
664 let mut w = 0.0;
665 let mut width = 0.0;
666 for run in runs {
667 let mut fnt = default_font.clone();
668 if let Some(font) = &run.font {
669 fnt = font.clone();
670 }
671 if let Some(i) = run.text.find(['\n', '\r'].as_ref()) {
672 let first = &run.text[..i];
673 let rest = &run.text[i + 1..];
674 w += calc_text_width(&fnt, first);
675 if w > width {
676 width = w;
677 }
678 w = calc_text_width(&fnt, rest);
679 } else {
680 w += calc_text_width(&fnt, &run.text);
681 }
682 }
683 if w > width {
684 width = w;
685 }
686 width
687}
688
689#[cfg(test)]
690mod tests {
691 use super::*;
692
693 #[test]
694 fn col_width_round_trip() {
695 let f = File::new_with_options(crate::options::Options::default());
696 f.set_col_width("Sheet1", "A", "C", 20.0).unwrap();
697 assert_eq!(f.get_col_width("Sheet1", "B").unwrap(), 20.0);
698 assert_eq!(f.get_col_width("Sheet1", "D").unwrap(), DEFAULT_COL_WIDTH);
699 }
700
701 #[test]
702 fn col_visible_round_trip() {
703 let f = File::new_with_options(crate::options::Options::default());
704 f.set_col_visible("Sheet1", "B", "B", false).unwrap();
705 assert!(!f.get_col_visible("Sheet1", "B").unwrap());
706 assert!(f.get_col_visible("Sheet1", "A").unwrap());
707 }
708
709 #[test]
710 fn cols_iterator_and_get_cols() {
711 let f = File::new_with_options(crate::options::Options::default());
712 f.set_cell_str("Sheet1", "A1", "a").unwrap();
713 f.set_cell_str("Sheet1", "B2", "b").unwrap();
714
715 let mut cols = f.cols("Sheet1").unwrap();
716 assert!(cols.next());
717 assert_eq!(cols.rows(Options::default()).unwrap(), vec!["a", ""]);
718 assert!(cols.next());
719 assert_eq!(cols.rows(Options::default()).unwrap(), vec!["", "b"]);
720 assert!(!cols.next());
721
722 let all = f.get_cols("Sheet1", Options::default()).unwrap();
723 assert_eq!(all, vec![vec!["a", ""], vec!["", "b"]]);
724 }
725
726 #[test]
727 fn col_style_round_trip() {
728 let f = File::new_with_options(crate::options::Options::default());
729 f.set_col_style("Sheet1", "B", "D", 0).unwrap();
730 assert_eq!(f.get_col_style("Sheet1", "C").unwrap(), 0);
731 }
732
733 #[test]
734 fn auto_fit_col_width_basic() {
735 let f = File::new_with_options(crate::options::Options::default());
736 f.set_cell_str("Sheet1", "A1", "Hello, World!").unwrap();
737 let before = f.get_col_width("Sheet1", "A").unwrap();
738 f.auto_fit_col_width("Sheet1", "A").unwrap();
739 let after = f.get_col_width("Sheet1", "A").unwrap();
740 assert!(after > before);
741 }
742}