From 12529d8472e8e6ab36e551b090f2205f85ba2916 Mon Sep 17 00:00:00 2001 From: Kawaljeet Singh <49296873+justkawal@users.noreply.github.com> Date: Sun, 12 Mar 2023 14:41:32 +0530 Subject: [PATCH 1/2] Last final testing for automated publishing. (#217) --- .github/workflows/dart.yml | 4 ++-- CHANGELOG.md | 4 ++++ README.md | 2 +- pubspec.yaml | 2 +- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/.github/workflows/dart.yml b/.github/workflows/dart.yml index f823a16d..a17d43e6 100644 --- a/.github/workflows/dart.yml +++ b/.github/workflows/dart.yml @@ -7,9 +7,9 @@ name: Dart on: push: - branches: [ null-safety ] + branches: [ developement ] pull_request: - branches: [ null-safety, publish ] + branches: [ developement, publish ] jobs: build: diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a207d5d..3847d2f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,10 @@ # Changelog All notable changes to this project will be documented in this file. +## [2.0.4] - 2023-03-12 +### Improved +- Automated Publishing. + ## [2.0.3] - 2023-03-12 ### Improved - Readme updated. diff --git a/README.md b/README.md index 5a9c3565..69ff209e 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ Add this to your package's `pubspec.yaml` file: ```yaml dependencies: - excel: 2.0.3 + excel: 2.0.4 ``` ### 2. Install it diff --git a/pubspec.yaml b/pubspec.yaml index b6a2ecba..7955c03d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: excel description: A flutter and dart library for reading, creating, editing and updating excel sheets with compatible both on client and server side. -version: 2.0.3 +version: 2.0.4 homepage: https://github.com/justkawal/excel environment: From 944866f8b7749b85d7fa8df4416d21b099151619 Mon Sep 17 00:00:00 2001 From: Faucon <49079695+FauconSpartiate@users.noreply.github.com> Date: Fri, 31 Mar 2023 13:16:29 +0200 Subject: [PATCH 2/2] Bump to 2.1.0 (#227) Co-authored-by: Kawaljeet Singh Co-authored-by: Kawaljeet Singh <49296873+justkawal@users.noreply.github.com> Co-authored-by: Yann Gauteron <37099668+amigne@users.noreply.github.com> Co-authored-by: Yann Gauteron --- .github/workflows/dart.yml | 4 +- CHANGELOG.md | 9 +- README.md | 53 +- lib/excel.dart | 2 + lib/src/excel.dart | 1 + lib/src/parser/parse.dart | 64 +++ lib/src/save/save_file.dart | 919 ++++++++++++++++-------------- lib/src/sheet/border_style.dart | 98 ++++ lib/src/sheet/cell_style.dart | 133 +++++ lib/src/sheet/header_footer.dart | 205 ++----- lib/src/sheet/sheet.dart | 13 +- pubspec.yaml | 3 +- test/excel_test.dart | 117 ++++ test/test_resources/borders.xlsx | Bin 0 -> 10822 bytes test/test_resources/borders2.xlsx | Bin 0 -> 10918 bytes 15 files changed, 1041 insertions(+), 580 deletions(-) create mode 100644 lib/src/sheet/border_style.dart create mode 100644 test/test_resources/borders.xlsx create mode 100644 test/test_resources/borders2.xlsx diff --git a/.github/workflows/dart.yml b/.github/workflows/dart.yml index a17d43e6..5bc36231 100644 --- a/.github/workflows/dart.yml +++ b/.github/workflows/dart.yml @@ -7,9 +7,9 @@ name: Dart on: push: - branches: [ developement ] + branches: [ main ] pull_request: - branches: [ developement, publish ] + branches: [ main, publish ] jobs: build: diff --git a/CHANGELOG.md b/CHANGELOG.md index 3847d2f2..fba882f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,13 @@ # Changelog All notable changes to this project will be documented in this file. +## [2.1.0] - 2023-03-30 +### Improved +- Add border functionality +### Fixed +- Fix Header and Footer with special characters +- Fix sheet.merge() + ## [2.0.4] - 2023-03-12 ### Improved - Automated Publishing. @@ -117,4 +124,4 @@ All notable changes to this project will be documented in this file. - TextWrapping and (Clip in Google Sheets) / (ShrinkToFit in Microsoft Excel) - Horizontal and Vertical Alignment - Update Cell by Cell-Name ("A1") -- Minor Bug Fixes \ No newline at end of file +- Minor Bug Fixes diff --git a/README.md b/README.md index 69ff209e..64c29736 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ Add this to your package's `pubspec.yaml` file: ```yaml dependencies: - excel: 2.0.4 + excel: 2.1.0 ``` ### 2. Install it @@ -228,7 +228,7 @@ Use `FilePicker` to pick files in Flutter Web. [FilePicker](https://pub.dev/pack ### Cell-Style Options | key | description | -| ------------------ | --------------------------------------------------------------------------------------------------------------------------------------- | +|--------------------|-----------------------------------------------------------------------------------------------------------------------------------------| | fontFamily | eg. getFontFamily(`FontFamily.Arial`) or getFontFamily(`FontFamily.Comic_Sans_MS`) `There is total 182 Font Families available for now` | | fontSize | specify the font-size as integer eg. fontSize = 15 | | bold | makes text bold - when set to `true`, by-default it is set to `false` | @@ -240,6 +240,55 @@ Use `FilePicker` to pick files in Flutter Web. [FilePicker](https://pub.dev/pack | wrap | Text wrapping `enum TextWrapping { WrapText, Clip }` eg. TextWrapping.Clip | | verticalAlign | align text vertically `enum VerticalAlign { Top, Center, Bottom }` eg. VerticalAlign.Top | | horizontalAlign | align text horizontally `enum HorizontalAlign { Left, Center, Right }` eg. HorizontalAlign.Right | +| leftBorder | the left border of the cell (see below) | +| rightBorder | the right border of the cell | +| topBorder | the top border of the cell | +| bottomBorder | the bottom border of the cell | +| diagonalBorder | the diagonal "border" of the cell | +| diagonalBorderUp | boolean value indicating if the diagonal "border" should be displayed on the up diagonal | +| diagonalBorderDown | boolean value indicating if the diagonal "border" should be displayed on the down diagonal | + +### Borders +Borders are defined for each side (left, right, top, and bottom) of the cell. Both diagonals (up and down) share the +same settings. A boolean value `true` must be set to either `diagonalBorderUp` or `diagonalBorderDown` (or both) to +display the desired diagonal. + +Each border must be a `Border` object. This object accepts two parameters : `borderStyle` to select one of the different +supported styles and `borderColorHex` to change the border color. + +The `borderStyle` must be a value from the enumeration`BorderStyle`: +* `BorderStyle.None` +* `BorderStyle.DashDot` +* `BorderStyle.DashDotDot` +* `BorderStyle.Dashed` +* `BorderStyle.Dotted` +* `BorderStyle.Double` +* `BorderStyle.Hair` +* `BorderStyle.Medium` +* `BorderStyle.MediumDashDot` +* `BorderStyle.MediumDashDotDot` +* `BorderStyle.MediumDashed` +* `BorderStyle.SlantDashDot` +* `BorderStyle.Thick` +* `BorderStyle.Thin` + + +```dart + /* + * + * Defines thin borders on the left and right of the cell, red thin border on the top + * and blue medium border on the bottom. + * + */ + + CellStyle cellStyle = CellStyle( + leftBorder: Border(borderStyle: BorderStyle.Thin), + rightBorder: Border(borderStyle: BorderStyle.Thin), + topBorder: Border(borderStyle: BorderStyle.Thin, borderColorHex: 'FFFF0000'), + bottomBorder: Border(borderStyle: BorderStyle.Medium, borderColorHex: 'FF0000FF'), + ); +``` + ### Make sheet RTL diff --git a/lib/excel.dart b/lib/excel.dart index 41e6bc69..bf06bb59 100644 --- a/lib/excel.dart +++ b/lib/excel.dart @@ -4,6 +4,7 @@ import 'dart:convert'; import 'dart:math'; import 'dart:typed_data'; import 'package:archive/archive.dart'; +import 'package:collection/collection.dart'; import 'package:equatable/equatable.dart'; import 'package:xml/xml.dart'; import 'src/web_helper/client_save_excel.dart' @@ -37,3 +38,4 @@ part 'src/sheet/cell_index.dart'; part 'src/sheet/cell_style.dart'; part 'src/sheet/font_style.dart'; part 'src/sheet/header_footer.dart'; +part 'src/sheet/border_style.dart'; diff --git a/lib/src/excel.dart b/lib/src/excel.dart index f1673b73..cd204adf 100644 --- a/lib/src/excel.dart +++ b/lib/src/excel.dart @@ -40,6 +40,7 @@ class Excel { late List _rtlChangeLook; late List<_FontStyle> _fontStyleList; late List _numFormats; + late List<_BorderSet> _borderSetList; late _SharedStringsMaintainer _sharedStrings; diff --git a/lib/src/parser/parse.dart b/lib/src/parser/parse.dart index 00549c47..c075d7e6 100644 --- a/lib/src/parser/parse.dart +++ b/lib/src/parser/parse.dart @@ -226,6 +226,7 @@ class Parser { _excel._fontStyleList = <_FontStyle>[]; _excel._patternFill = []; _excel._cellStyleList = []; + _excel._borderSetList = <_BorderSet>[]; Iterable fontList = document.findAllElements('font'); @@ -241,12 +242,63 @@ class Parser { } }); + document.findAllElements('border').forEach((node) { + final diagonalUp = !['0', 'false', null] + .contains(node.getAttribute('diagonalUp')?.trim()); + final diagonalDown = !['0', 'false', null] + .contains(node.getAttribute('diagonalDown')?.trim()); + + const List borderElementNamesList = [ + 'left', + 'right', + 'top', + 'bottom', + 'diagonal' + ]; + Map borderElements = {}; + for (var elementName in borderElementNamesList) { + XmlElement? element; + try { + element = node.findElements(elementName).single; + } on StateError catch (_) { + // Either there is no element, or there are too many ones. + // Silently ignore this element. + } + + final borderStyleAttribute = element?.getAttribute('style')?.trim(); + final borderStyle = borderStyleAttribute != null + ? getBorderStyleByName(borderStyleAttribute) + : null; + + String? borderColorHex; + try { + final color = element?.findElements('color').single; + borderColorHex = color?.getAttribute('rgb')?.trim(); + } on StateError catch (_) {} + + borderElements[elementName] = + Border(borderStyle: borderStyle, borderColorHex: borderColorHex); + } + + final borderSet = _BorderSet( + leftBorder: borderElements['left']!, + rightBorder: borderElements['right']!, + topBorder: borderElements['top']!, + bottomBorder: borderElements['bottom']!, + diagonalBorder: borderElements['diagonal']!, + diagonalBorderDown: diagonalDown, + diagonalBorderUp: diagonalUp, + ); + _excel._borderSetList.add(borderSet); + }); + document.findAllElements('cellXfs').forEach((node1) { node1.findAllElements('xf').forEach((node) { _excel._numFormats.add(_getFontIndex(node, 'numFmtId')); String fontColor = "FF000000", backgroundColor = "none"; String? fontFamily; + _BorderSet? borderSet; int fontSize = 12; bool isBold = false, isItalic = false; @@ -321,6 +373,11 @@ class Parser { backgroundColor = _excel._patternFill[fillId]; } + int borderId = _getFontIndex(node, 'borderId'); + if (borderId < _excel._borderSetList.length) { + borderSet = _excel._borderSetList[borderId]; + } + if (node.children.isNotEmpty) { node.findElements('alignment').forEach((child) { if (_getFontIndex(child, 'wrapText') == 1) { @@ -366,6 +423,13 @@ class Parser { verticalAlign: verticalAlign, textWrapping: textWrapping, rotation: rotation, + leftBorder: borderSet?.leftBorder, + rightBorder: borderSet?.rightBorder, + topBorder: borderSet?.topBorder, + bottomBorder: borderSet?.bottomBorder, + diagonalBorder: borderSet?.diagonalBorder, + diagonalBorderUp: borderSet?.diagonalBorderUp ?? false, + diagonalBorderDown: borderSet?.diagonalBorderDown ?? false, ); _excel._cellStyleList.add(cellStyle); diff --git a/lib/src/save/save_file.dart b/lib/src/save/save_file.dart index 2cba18ff..1de601dd 100644 --- a/lib/src/save/save_file.dart +++ b/lib/src/save/save_file.dart @@ -10,201 +10,6 @@ class Save { _innerCellStyle = []; } - List? _save() { - if (_excel._colorChanges) { - _processStylesFile(); - } - _setSheetElements(); - if (_excel._defaultSheet != null) { - _setDefaultSheet(_excel._defaultSheet); - } - _setSharedStrings(); - - if (_excel._mergeChanges) { - _setMerge(); - } - - if (_excel._rtlChanges) { - _setRTL(); - } - - for (var xmlFile in _excel._xmlFiles.keys) { - var xml = _excel._xmlFiles[xmlFile].toString(); - var content = utf8.encode(xml); - _archiveFiles[xmlFile] = ArchiveFile(xmlFile, content.length, content); - } - return ZipEncoder().encode(_cloneArchive(_excel._archive)); - } - - Archive _cloneArchive(Archive archive) { - var clone = Archive(); - archive.files.forEach((file) { - if (file.isFile) { - ArchiveFile copy; - if (_archiveFiles.containsKey(file.name)) { - copy = _archiveFiles[file.name]!; - } else { - var content = file.content as Uint8List; - var compress = !_noCompression.contains(file.name); - copy = ArchiveFile(file.name, content.length, content) - ..compress = compress; - } - clone.addFile(copy); - } - }); - return clone; - } - - bool _setDefaultSheet(String? sheetName) { - if (sheetName == null || _excel._xmlFiles['xl/workbook.xml'] == null) { - return false; - } - List sheetList = - _excel._xmlFiles['xl/workbook.xml']!.findAllElements('sheet').toList(); - XmlElement elementFound = XmlElement(XmlName('')); - - int position = -1; - for (int i = 0; i < sheetList.length; i++) { - var _sheetName = sheetList[i].getAttribute('name'); - if (_sheetName != null && _sheetName.toString() == sheetName) { - elementFound = sheetList[i]; - position = i; - break; - } - } - - if (position == -1) { - return false; - } - if (position == 0) { - return true; - } - - _excel._xmlFiles['xl/workbook.xml']! - .findAllElements('sheets') - .first - .children - ..removeAt(position) - ..insert(0, elementFound); - - String? expectedSheet = _excel._getDefaultSheet(); - - return expectedSheet == sheetName; - } - - /// Writing cell contained text into the excel sheet files. - _setSheetElements() { - _excel._sharedStrings = _SharedStringsMaintainer.instance; - _excel._sharedStrings.clear(); - - _excel._sheetMap.forEach((sheet, value) { - /// - /// Create the sheet's xml file if it does not exist. - if (_excel._sheets[sheet] == null) { - parser._createSheet(sheet); - } - - /// Clear the previous contents of the sheet if it exists, - /// in order to reduce the time to find and compare with the sheet rows - /// and hence just do the work of putting the data only i.e. creating new rows - if (_excel._sheets[sheet]?.children.isNotEmpty ?? false) { - _excel._sheets[sheet]!.children.clear(); - } - - _setColumnWidth(sheet); - - /// `Above function is important in order to wipe out the old contents of the sheet.` - for (var rowIndex = 0; rowIndex < value._maxRows; rowIndex++) { - if (value._sheetData[rowIndex] == null) { - continue; - } - var foundRow = - _createNewRow(_excel._sheets[sheet]! as XmlElement, rowIndex); - for (var colIndex = 0; colIndex < value._maxCols; colIndex++) { - var data = value._sheetData[rowIndex]![colIndex]; - if (data == null) { - continue; - } - if (data.value != null) { - _updateCell(sheet, foundRow, colIndex, rowIndex, data.value); - } - } - } - - _setHeaderFooter(sheet); - }); - } - - _setColumnWidth(String sheetName) { - final sheetObject = _excel._sheetMap[sheetName]; - if (sheetObject == null) return; - - var xmlFile = _excel._xmlFiles[_excel._xmlSheetId[sheetName]]; - if (xmlFile == null) return; - - final colElements = xmlFile.findAllElements('cols'); - - if (sheetObject.getColWidths.isEmpty && - sheetObject.getColAutoFits.isEmpty) { - if (colElements.isEmpty) { - return; - } - - final cols = colElements.first; - final worksheet = xmlFile.findAllElements('worksheet').first; - worksheet.children.remove(cols); - return; - } - - if (colElements.isEmpty) { - final worksheet = xmlFile.findAllElements('worksheet').first; - final sheetData = xmlFile.findAllElements('sheetData').first; - final index = worksheet.children.indexOf(sheetData); - - worksheet.children.insert(index, XmlElement(XmlName('cols'), [], [])); - } - - var cols = colElements.first; - - if (cols.children.isNotEmpty) { - cols.children.clear(); - } - - final autoFits = sheetObject.getColAutoFits.asMap(); - final customWidths = sheetObject.getColWidths.asMap(); - - final columnCount = max(autoFits.length, customWidths.length); - - List colWidths = []; - int min = 0; - - for (var index = 0; index < columnCount; index++) { - double value = _defaultColumnWidth; - - if (autoFits.containsKey(index) && - autoFits[index] == true && - (!customWidths.containsKey(index) || - customWidths[index] == _defaultColumnWidth)) { - value = _calcAutoFitColWidth(sheetObject, index); - } else { - if (customWidths.containsKey(index)) { - value = customWidths[index]!; - } - } - - colWidths.add(value); - - if (index != 0 && colWidths[index - 1] != value) { - _addNewCol(cols, min, index - 1, colWidths[index - 1]); - min = index; - } - - if (index == (columnCount - 1)) { - _addNewCol(cols, index, index, value); - } - } - } - void _addNewCol(XmlElement cols, int min, int max, double value) { cols.children.add(XmlElement(XmlName('col'), [ XmlAttribute(XmlName('min'), (min + 1).toString()), @@ -227,130 +32,101 @@ class Save { return ((maxNumOfCharacters * 7.0 + 9.0) / 7.0 * 256).truncate() / 256; } - _setRTL() { - _excel._rtlChangeLook.forEach((s) { - var sheetObject = _excel._sheetMap[s]; - if (sheetObject != null && - _excel._xmlSheetId.containsKey(s) && - _excel._xmlFiles.containsKey(_excel._xmlSheetId[s])) { - var itrSheetViewsRTLElement = _excel._xmlFiles[_excel._xmlSheetId[s]] - ?.findAllElements('sheetViews'); - - if (itrSheetViewsRTLElement?.isNotEmpty ?? false) { - var itrSheetViewRTLElement = _excel._xmlFiles[_excel._xmlSheetId[s]] - ?.findAllElements('sheetView'); - - if (itrSheetViewRTLElement?.isNotEmpty ?? false) { - /// clear all the children of the sheetViews here - - _excel._xmlFiles[_excel._xmlSheetId[s]] - ?.findAllElements('sheetViews') - .first - .children - .clear(); - } - - _excel._xmlFiles[_excel._xmlSheetId[s]] - ?.findAllElements('sheetViews') - .first - .children - .add(XmlElement( - XmlName('sheetView'), - [ - if (sheetObject.isRTL) - XmlAttribute(XmlName('rightToLeft'), '1'), - XmlAttribute(XmlName('workbookViewId'), '0'), - ], - )); + Archive _cloneArchive(Archive archive) { + var clone = Archive(); + archive.files.forEach((file) { + if (file.isFile) { + ArchiveFile copy; + if (_archiveFiles.containsKey(file.name)) { + copy = _archiveFiles[file.name]!; } else { - _excel._xmlFiles[_excel._xmlSheetId[s]] - ?.findAllElements('worksheet') - .first - .children - .add(XmlElement(XmlName('sheetViews'), [], [ - XmlElement( - XmlName('sheetView'), - [ - if (sheetObject.isRTL) - XmlAttribute(XmlName('rightToLeft'), '1'), - XmlAttribute(XmlName('workbookViewId'), '0'), - ], - ) - ])); + var content = file.content as Uint8List; + var compress = !_noCompression.contains(file.name); + copy = ArchiveFile(file.name, content.length, content) + ..compress = compress; } + clone.addFile(copy); } }); + return clone; } - /// Writing the merged cells information into the excel properties files. - _setMerge() { - _selfCorrectSpanMap(_excel); - _excel._mergeChangeLook.forEach((s) { - if (_excel._sheetMap[s] != null && - _excel._sheetMap[s]!._spanList.isNotEmpty && - _excel._xmlSheetId.containsKey(s) && - _excel._xmlFiles.containsKey(_excel._xmlSheetId[s])) { - Iterable? iterMergeElement = _excel - ._xmlFiles[_excel._xmlSheetId[s]] - ?.findAllElements('mergeCells'); - late XmlElement mergeElement; - if (iterMergeElement?.isNotEmpty ?? false) { - mergeElement = iterMergeElement!.first; - } else { - if ((_excel._xmlFiles[_excel._xmlSheetId[s]] - ?.findAllElements('worksheet') - .length ?? - 0) > - 0) { - int index = _excel._xmlFiles[_excel._xmlSheetId[s]]! - .findAllElements('worksheet') - .first - .children - .indexOf(_excel._xmlFiles[_excel._xmlSheetId[s]]! - .findAllElements("sheetData") - .first); - if (index == -1) { - _damagedExcel(); - } - _excel._xmlFiles[_excel._xmlSheetId[s]]! - .findAllElements('worksheet') - .first - .children - .insert( - index + 1, - XmlElement(XmlName('mergeCells'), - [XmlAttribute(XmlName('count'), '0')])); + /* XmlElement _replaceCell(String sheet, XmlElement row, XmlElement lastCell, + int columnIndex, int rowIndex, dynamic value) { + var index = lastCell == null ? 0 : row.children.indexOf(lastCell); + var cell = _createCell(sheet, columnIndex, rowIndex, value); + row.children + ..removeAt(index) + ..insert(index, cell); + return cell; + } */ - mergeElement = _excel._xmlFiles[_excel._xmlSheetId[s]]! - .findAllElements('mergeCells') - .first; - } else { - _damagedExcel(); - } - } + // Manage value's type + XmlElement _createCell( + String sheet, int columnIndex, int rowIndex, dynamic value) { + if (value is SharedString) { + _excel._sharedStrings.add(value); + } - List _spannedItems = - List.from(_excel._sheetMap[s]!.spannedItems); + String rC = getCellId(columnIndex, rowIndex); - [ - ['count', _spannedItems.length.toString()], - ].forEach((value) { - if (mergeElement.getAttributeNode(value[0]) == null) { - mergeElement.attributes - .add(XmlAttribute(XmlName(value[0]), value[1])); - } else { - mergeElement.getAttributeNode(value[0])!.value = value[1]; - } - }); + var attributes = [ + XmlAttribute(XmlName('r'), rC), + if (value is SharedString) XmlAttribute(XmlName('t'), 's'), + ]; + + if (_excel._colorChanges && + (_excel._sheetMap[sheet]?._sheetData != null) && + _excel._sheetMap[sheet]!._sheetData[rowIndex] != null && + _excel._sheetMap[sheet]!._sheetData[rowIndex]![columnIndex] + ?.cellStyle != + null) { + CellStyle cellStyle = _excel + ._sheetMap[sheet]!._sheetData[rowIndex]![columnIndex]!.cellStyle!; + int upperLevelPos = _checkPosition(_excel._cellStyleList, cellStyle); + if (upperLevelPos == -1) { + int lowerLevelPos = _checkPosition(_innerCellStyle, cellStyle); + if (lowerLevelPos != -1) { + upperLevelPos = lowerLevelPos + _excel._cellStyleList.length; + } else { + upperLevelPos = 0; + } + } + attributes.insert( + 1, + XmlAttribute(XmlName('s'), '$upperLevelPos'), + ); + } else if (_excel._cellStyleReferenced.containsKey(sheet) && + _excel._cellStyleReferenced[sheet]!.containsKey(rC)) { + attributes.insert( + 1, + XmlAttribute( + XmlName('s'), '${_excel._cellStyleReferenced[sheet]![rC]}'), + ); + } - mergeElement.children.clear(); + var children = value == null + ? [] + : [ + if (value is Formula) + XmlElement(XmlName('f'), [], [XmlText(value.formula.toString())]), + XmlElement(XmlName('v'), [], [ + XmlText(value is SharedString + ? _excel._sharedStrings.indexOf(value).toString() + : value is Formula + ? '' + : value.toString()) + ]), + ]; + return XmlElement(XmlName('c'), attributes, children); + } - _spannedItems.forEach((value) { - mergeElement.children.add(XmlElement(XmlName('mergeCell'), - [XmlAttribute(XmlName('ref'), '$value')], [])); - }); - } - }); + /// + XmlElement _createNewRow(XmlElement table, int rowIndex) { + var row = XmlElement(XmlName('row'), + [XmlAttribute(XmlName('r'), (rowIndex + 1).toString())], []); + table.children.add(row); + return row; } /// Writing Font Color in [xl/styles.xml] from the Cells of the sheets. @@ -359,6 +135,7 @@ class Save { _innerCellStyle = []; List innerPatternFill = []; List<_FontStyle> innerFontStyle = <_FontStyle>[]; + List<_BorderSet> innerBorderSet = <_BorderSet>[]; _excel._sheetMap.forEach((sheetName, sheetObject) { sheetObject._sheetData.forEach((_, colMap) { @@ -388,12 +165,18 @@ class Save { innerFontStyle.add(_fs); } - /// Filling the inner usable extra list of backgroung color + /// Filling the inner usable extra list of background color String backgroundColor = cellStyle.backgroundColor; if (!_excel._patternFill.contains(backgroundColor) && !innerPatternFill.contains(backgroundColor)) { innerPatternFill.add(backgroundColor); } + + final _bs = _createBorderSetFromCellStyle(cellStyle); + if (!_excel._borderSetList.contains(_bs) && + !innerBorderSet.contains(_bs)) { + innerBorderSet.add(_bs); + } }); XmlElement fonts = @@ -495,6 +278,53 @@ class Save { } }); + XmlElement borders = + _excel._xmlFiles['xl/styles.xml']!.findAllElements('borders').first; + var borderAttribute = borders.getAttributeNode('count'); + + if (borderAttribute != null) { + borderAttribute.value = + '${_excel._borderSetList.length + innerBorderSet.length}'; + } else { + borders.attributes.add(XmlAttribute(XmlName('count'), + '${_excel._borderSetList.length + innerBorderSet.length}')); + } + + innerBorderSet.forEach((border) { + var borderElement = XmlElement(XmlName('border')); + if (border.diagonalBorderDown) { + borderElement.attributes + .add(XmlAttribute(XmlName('diagonalDown'), '1')); + } + if (border.diagonalBorderUp) { + borderElement.attributes.add(XmlAttribute(XmlName('diagonalUp'), '1')); + } + final Map borderMap = { + 'left': border.leftBorder, + 'right': border.rightBorder, + 'top': border.topBorder, + 'bottom': border.bottomBorder, + 'diagonal': border.diagonalBorder, + }; + for (var key in borderMap.keys) { + final borderValue = borderMap[key]!; + + final element = XmlElement(XmlName(key)); + final style = borderValue.borderStyle; + if (style != null) { + element.attributes.add(XmlAttribute(XmlName('style'), style.style)); + } + final color = borderValue.borderColorHex; + if (color != null) { + element.children.add(XmlElement( + XmlName('color'), [XmlAttribute(XmlName('rgb'), color)])); + } + borderElement.children.add(element); + } + + borders.children.add(borderElement); + }); + XmlElement celx = _excel._xmlFiles['xl/styles.xml']!.findAllElements('cellXfs').first; var cellAttribute = celx.getAttributeNode('count'); @@ -518,15 +348,18 @@ class Save { fontSize: cellStyle.fontSize, fontFamily: cellStyle.fontFamily); - HorizontalAlign horizontalALign = cellStyle.horizontalAlignment; + HorizontalAlign horizontalAlign = cellStyle.horizontalAlignment; VerticalAlign verticalAlign = cellStyle.verticalAlignment; int rotation = cellStyle.rotation; TextWrapping? textWrapping = cellStyle.wrap; int backgroundIndex = innerPatternFill.indexOf(backgroundColor), fontIndex = _fontStyleIndex(innerFontStyle, _fs); + _BorderSet _bs = _createBorderSetFromCellStyle(cellStyle); + int borderIndex = innerBorderSet.indexOf(_bs); var attributes = [ - XmlAttribute(XmlName('borderId'), '0'), + XmlAttribute(XmlName('borderId'), + '${borderIndex == -1 ? 0 : borderIndex + _excel._borderSetList.length}'), XmlAttribute(XmlName('fillId'), '${backgroundIndex == -1 ? 0 : backgroundIndex + _excel._patternFill.length}'), XmlAttribute(XmlName('fontId'), @@ -550,7 +383,7 @@ class Save { var children = []; - if (horizontalALign != HorizontalAlign.Left || + if (horizontalAlign != HorizontalAlign.Left || textWrapping != null || verticalAlign != VerticalAlign.Bottom || rotation != 0) { @@ -570,9 +403,9 @@ class Save { childAttributes.add(XmlAttribute(XmlName('vertical'), '$ver')); } - if (horizontalALign != HorizontalAlign.Left) { + if (horizontalAlign != HorizontalAlign.Left) { String hor = - horizontalALign == HorizontalAlign.Right ? 'right' : 'center'; + horizontalAlign == HorizontalAlign.Right ? 'right' : 'center'; childAttributes.add(XmlAttribute(XmlName('horizontal'), '$hor')); } if (rotation != 0) { @@ -587,34 +420,225 @@ class Save { }); } - /// Writing the value of excel cells into the separate - /// sharedStrings file so as to minimize the size of excel files. - _setSharedStrings() { - var uniqueCount = 0; - var count = 0; + List? _save() { + if (_excel._colorChanges) { + _processStylesFile(); + } + _setSheetElements(); + if (_excel._defaultSheet != null) { + _setDefaultSheet(_excel._defaultSheet); + } + _setSharedStrings(); + + if (_excel._mergeChanges) { + _setMerge(); + } + + if (_excel._rtlChanges) { + _setRTL(); + } + + for (var xmlFile in _excel._xmlFiles.keys) { + var xml = _excel._xmlFiles[xmlFile].toString(); + var content = utf8.encode(xml); + _archiveFiles[xmlFile] = ArchiveFile(xmlFile, content.length, content); + } + return ZipEncoder().encode(_cloneArchive(_excel._archive)); + } + + _setColumnWidth(String sheetName) { + final sheetObject = _excel._sheetMap[sheetName]; + if (sheetObject == null) return; + + var xmlFile = _excel._xmlFiles[_excel._xmlSheetId[sheetName]]; + if (xmlFile == null) return; + + final colElements = xmlFile.findAllElements('cols'); + + if (sheetObject.getColWidths.isEmpty && + sheetObject.getColAutoFits.isEmpty) { + if (colElements.isEmpty) { + return; + } + + final cols = colElements.first; + final worksheet = xmlFile.findAllElements('worksheet').first; + worksheet.children.remove(cols); + return; + } + + if (colElements.isEmpty) { + final worksheet = xmlFile.findAllElements('worksheet').first; + final sheetData = xmlFile.findAllElements('sheetData').first; + final index = worksheet.children.indexOf(sheetData); + + worksheet.children.insert(index, XmlElement(XmlName('cols'), [], [])); + } + + var cols = colElements.first; + + if (cols.children.isNotEmpty) { + cols.children.clear(); + } + + final autoFits = sheetObject.getColAutoFits.asMap(); + final customWidths = sheetObject.getColWidths.asMap(); + + final columnCount = max(autoFits.length, customWidths.length); + + List colWidths = []; + int min = 0; + + for (var index = 0; index < columnCount; index++) { + double value = _defaultColumnWidth; + + if (autoFits.containsKey(index) && + autoFits[index] == true && + (!customWidths.containsKey(index) || + customWidths[index] == _defaultColumnWidth)) { + value = _calcAutoFitColWidth(sheetObject, index); + } else { + if (customWidths.containsKey(index)) { + value = customWidths[index]!; + } + } + + colWidths.add(value); + + if (index != 0 && colWidths[index - 1] != value) { + _addNewCol(cols, min, index - 1, colWidths[index - 1]); + min = index; + } + + if (index == (columnCount - 1)) { + _addNewCol(cols, index, index, value); + } + } + } + + bool _setDefaultSheet(String? sheetName) { + if (sheetName == null || _excel._xmlFiles['xl/workbook.xml'] == null) { + return false; + } + List sheetList = + _excel._xmlFiles['xl/workbook.xml']!.findAllElements('sheet').toList(); + XmlElement elementFound = XmlElement(XmlName('')); + + int position = -1; + for (int i = 0; i < sheetList.length; i++) { + var _sheetName = sheetList[i].getAttribute('name'); + if (_sheetName != null && _sheetName.toString() == sheetName) { + elementFound = sheetList[i]; + position = i; + break; + } + } + + if (position == -1) { + return false; + } + if (position == 0) { + return true; + } + + _excel._xmlFiles['xl/workbook.xml']! + .findAllElements('sheets') + .first + .children + ..removeAt(position) + ..insert(0, elementFound); + + String? expectedSheet = _excel._getDefaultSheet(); + + return expectedSheet == sheetName; + } + + void _setHeaderFooter(String sheetName) { + final sheet = _excel._sheetMap[sheetName]; + if (sheet == null) return; + + final xmlFile = _excel._xmlFiles[_excel._xmlSheetId[sheetName]]; + if (xmlFile == null) return; + + final sheetXmlElement = xmlFile.findAllElements("worksheet").first; + + final results = sheetXmlElement.findAllElements("headerFooter"); + if (results.isNotEmpty) { + sheetXmlElement.children.remove(results.first); + } + + if (sheet.headerFooter == null) return; + + sheetXmlElement.children.add(sheet.headerFooter!.toXmlElement()); + } + + /// Writing the merged cells information into the excel properties files. + _setMerge() { + _selfCorrectSpanMap(_excel); + _excel._mergeChangeLook.forEach((s) { + if (_excel._sheetMap[s] != null && + _excel._sheetMap[s]!._spanList.isNotEmpty && + _excel._xmlSheetId.containsKey(s) && + _excel._xmlFiles.containsKey(_excel._xmlSheetId[s])) { + Iterable? iterMergeElement = _excel + ._xmlFiles[_excel._xmlSheetId[s]] + ?.findAllElements('mergeCells'); + late XmlElement mergeElement; + if (iterMergeElement?.isNotEmpty ?? false) { + mergeElement = iterMergeElement!.first; + } else { + if ((_excel._xmlFiles[_excel._xmlSheetId[s]] + ?.findAllElements('worksheet') + .length ?? + 0) > + 0) { + int index = _excel._xmlFiles[_excel._xmlSheetId[s]]! + .findAllElements('worksheet') + .first + .children + .indexOf(_excel._xmlFiles[_excel._xmlSheetId[s]]! + .findAllElements("sheetData") + .first); + if (index == -1) { + _damagedExcel(); + } + _excel._xmlFiles[_excel._xmlSheetId[s]]! + .findAllElements('worksheet') + .first + .children + .insert( + index + 1, + XmlElement(XmlName('mergeCells'), + [XmlAttribute(XmlName('count'), '0')])); - XmlElement shareString = _excel - ._xmlFiles['xl/${_excel._sharedStringsTarget}']! - .findAllElements('sst') - .first; + mergeElement = _excel._xmlFiles[_excel._xmlSheetId[s]]! + .findAllElements('mergeCells') + .first; + } else { + _damagedExcel(); + } + } - shareString.children.clear(); + List _spannedItems = + List.from(_excel._sheetMap[s]!.spannedItems); - _excel._sharedStrings._map.forEach((string, ss) { - uniqueCount += 1; - count += ss.count; + [ + ['count', _spannedItems.length.toString()], + ].forEach((value) { + if (mergeElement.getAttributeNode(value[0]) == null) { + mergeElement.attributes + .add(XmlAttribute(XmlName(value[0]), value[1])); + } else { + mergeElement.getAttributeNode(value[0])!.value = value[1]; + } + }); - shareString.children.add(string.node); - }); + mergeElement.children.clear(); - [ - ['count', '$count'], - ['uniqueCount', '$uniqueCount'] - ].forEach((value) { - if (shareString.getAttributeNode(value[0]) == null) { - shareString.attributes.add(XmlAttribute(XmlName(value[0]), value[1])); - } else { - shareString.getAttributeNode(value[0])!.value = value[1]; + _spannedItems.forEach((value) { + mergeElement.children.add(XmlElement(XmlName('mergeCell'), + [XmlAttribute(XmlName('ref'), '$value')], [])); + }); } }); } @@ -640,12 +664,12 @@ class Save { return row; } - + XmlElement _createRow(int rowIndex) { return XmlElement(XmlName('row'), [XmlAttribute(XmlName('r'), (rowIndex + 1).toString())], []); - } - + } + XmlElement __insertRow(XmlElement table, XmlElement lastRow, int rowIndex) { var row = _createRow(rowIndex); if (lastRow == null) { @@ -657,85 +681,137 @@ class Save { return row; }*/ - /// - XmlElement _createNewRow(XmlElement table, int rowIndex) { - var row = XmlElement(XmlName('row'), - [XmlAttribute(XmlName('r'), (rowIndex + 1).toString())], []); - table.children.add(row); - return row; + _setRTL() { + _excel._rtlChangeLook.forEach((s) { + var sheetObject = _excel._sheetMap[s]; + if (sheetObject != null && + _excel._xmlSheetId.containsKey(s) && + _excel._xmlFiles.containsKey(_excel._xmlSheetId[s])) { + var itrSheetViewsRTLElement = _excel._xmlFiles[_excel._xmlSheetId[s]] + ?.findAllElements('sheetViews'); + + if (itrSheetViewsRTLElement?.isNotEmpty ?? false) { + var itrSheetViewRTLElement = _excel._xmlFiles[_excel._xmlSheetId[s]] + ?.findAllElements('sheetView'); + + if (itrSheetViewRTLElement?.isNotEmpty ?? false) { + /// clear all the children of the sheetViews here + + _excel._xmlFiles[_excel._xmlSheetId[s]] + ?.findAllElements('sheetViews') + .first + .children + .clear(); + } + + _excel._xmlFiles[_excel._xmlSheetId[s]] + ?.findAllElements('sheetViews') + .first + .children + .add(XmlElement( + XmlName('sheetView'), + [ + if (sheetObject.isRTL) + XmlAttribute(XmlName('rightToLeft'), '1'), + XmlAttribute(XmlName('workbookViewId'), '0'), + ], + )); + } else { + _excel._xmlFiles[_excel._xmlSheetId[s]] + ?.findAllElements('worksheet') + .first + .children + .add(XmlElement(XmlName('sheetViews'), [], [ + XmlElement( + XmlName('sheetView'), + [ + if (sheetObject.isRTL) + XmlAttribute(XmlName('rightToLeft'), '1'), + XmlAttribute(XmlName('workbookViewId'), '0'), + ], + ) + ])); + } + } + }); } -/* XmlElement _replaceCell(String sheet, XmlElement row, XmlElement lastCell, - int columnIndex, int rowIndex, dynamic value) { - var index = lastCell == null ? 0 : row.children.indexOf(lastCell); - var cell = _createCell(sheet, columnIndex, rowIndex, value); - row.children - ..removeAt(index) - ..insert(index, cell); - return cell; - } */ + /// Writing the value of excel cells into the separate + /// sharedStrings file so as to minimize the size of excel files. + _setSharedStrings() { + var uniqueCount = 0; + var count = 0; - // Manage value's type - XmlElement _createCell( - String sheet, int columnIndex, int rowIndex, dynamic value) { - if (value is SharedString) { - _excel._sharedStrings.add(value); - } + XmlElement shareString = _excel + ._xmlFiles['xl/${_excel._sharedStringsTarget}']! + .findAllElements('sst') + .first; - String rC = getCellId(columnIndex, rowIndex); + shareString.children.clear(); - var attributes = [ - XmlAttribute(XmlName('r'), rC), - if (value is SharedString) XmlAttribute(XmlName('t'), 's'), - ]; + _excel._sharedStrings._map.forEach((string, ss) { + uniqueCount += 1; + count += ss.count; - if (_excel._colorChanges && - (_excel._sheetMap[sheet]?._sheetData != null) && - _excel._sheetMap[sheet]!._sheetData[rowIndex] != null && - _excel._sheetMap[sheet]!._sheetData[rowIndex]![columnIndex] - ?.cellStyle != - null) { - CellStyle cellStyle = _excel - ._sheetMap[sheet]!._sheetData[rowIndex]![columnIndex]!.cellStyle!; - int upperLevelPos = _checkPosition(_excel._cellStyleList, cellStyle); - if (upperLevelPos == -1) { - int lowerLevelPos = _checkPosition(_innerCellStyle, cellStyle); - if (lowerLevelPos != -1) { - upperLevelPos = lowerLevelPos + _excel._cellStyleList.length; - } else { - upperLevelPos = 0; + shareString.children.add(string.node); + }); + + [ + ['count', '$count'], + ['uniqueCount', '$uniqueCount'] + ].forEach((value) { + if (shareString.getAttributeNode(value[0]) == null) { + shareString.attributes.add(XmlAttribute(XmlName(value[0]), value[1])); + } else { + shareString.getAttributeNode(value[0])!.value = value[1]; + } + }); + } + + /// Writing cell contained text into the excel sheet files. + _setSheetElements() { + _excel._sharedStrings = _SharedStringsMaintainer.instance; + _excel._sharedStrings.clear(); + + _excel._sheetMap.forEach((sheet, value) { + /// + /// Create the sheet's xml file if it does not exist. + if (_excel._sheets[sheet] == null) { + parser._createSheet(sheet); + } + + /// Clear the previous contents of the sheet if it exists, + /// in order to reduce the time to find and compare with the sheet rows + /// and hence just do the work of putting the data only i.e. creating new rows + if (_excel._sheets[sheet]?.children.isNotEmpty ?? false) { + _excel._sheets[sheet]!.children.clear(); + } + + _setColumnWidth(sheet); + + /// `Above function is important in order to wipe out the old contents of the sheet.` + for (var rowIndex = 0; rowIndex < value._maxRows; rowIndex++) { + if (value._sheetData[rowIndex] == null) { + continue; + } + var foundRow = + _createNewRow(_excel._sheets[sheet]! as XmlElement, rowIndex); + for (var colIndex = 0; colIndex < value._maxCols; colIndex++) { + var data = value._sheetData[rowIndex]![colIndex]; + if (data == null) { + continue; + } + if (data.value != null) { + _updateCell(sheet, foundRow, colIndex, rowIndex, data.value); + } } } - attributes.insert( - 1, - XmlAttribute(XmlName('s'), '$upperLevelPos'), - ); - } else if (_excel._cellStyleReferenced.containsKey(sheet) && - _excel._cellStyleReferenced[sheet]!.containsKey(rC)) { - attributes.insert( - 1, - XmlAttribute( - XmlName('s'), '${_excel._cellStyleReferenced[sheet]![rC]}'), - ); - } - var children = value == null - ? [] - : [ - if (value is Formula) - XmlElement(XmlName('f'), [], [XmlText(value.formula.toString())]), - XmlElement(XmlName('v'), [], [ - XmlText(value is SharedString - ? _excel._sharedStrings.indexOf(value).toString() - : value is Formula - ? '' - : value.toString()) - ]), - ]; - return XmlElement(XmlName('c'), attributes, children); + _setHeaderFooter(sheet); + }); } -// slow implementation + // slow implementation /* XmlElement _updateCell(String sheet, XmlElement node, int columnIndex, int rowIndex, dynamic value) { XmlElement cell; @@ -765,22 +841,13 @@ class Save { return cell; } - void _setHeaderFooter(String sheetName) { - final sheet = _excel._sheetMap[sheetName]; - if (sheet == null) return; - - final xmlFile = _excel._xmlFiles[_excel._xmlSheetId[sheetName]]; - if (xmlFile == null) return; - - final sheetXmlElement = xmlFile.findAllElements("worksheet").first; - - final results = sheetXmlElement.findAllElements("headerFooter"); - if (results.isNotEmpty) { - sheetXmlElement.children.remove(results.first); - } - - if (sheet.headerFooter == null) return; - - sheetXmlElement.children.add(sheet.headerFooter!.toXmlElement()); - } + _BorderSet _createBorderSetFromCellStyle(CellStyle cellStyle) => _BorderSet( + leftBorder: cellStyle.leftBorder, + rightBorder: cellStyle.rightBorder, + topBorder: cellStyle.topBorder, + bottomBorder: cellStyle.bottomBorder, + diagonalBorder: cellStyle.diagonalBorder, + diagonalBorderUp: cellStyle.diagonalBorderUp, + diagonalBorderDown: cellStyle.diagonalBorderDown, + ); } diff --git a/lib/src/sheet/border_style.dart b/lib/src/sheet/border_style.dart new file mode 100644 index 00000000..3df1426d --- /dev/null +++ b/lib/src/sheet/border_style.dart @@ -0,0 +1,98 @@ +part of excel; + +class Border extends Equatable { + late final BorderStyle? borderStyle; + late final String? borderColorHex; + + Border({BorderStyle? borderStyle, String? borderColorHex}) { + this.borderStyle = borderStyle == BorderStyle.None ? null : borderStyle; + this.borderColorHex = + borderColorHex != null ? _isColorAppropriate(borderColorHex) : null; + } + + @override + String toString() { + return 'Border(borderStyle: $borderStyle, borderColorHex: $borderColorHex)'; + } + + @override + List get props => [ + borderStyle, + borderColorHex, + ]; +} + +class _BorderSet extends Equatable { + late final Border leftBorder; + late final Border rightBorder; + late final Border topBorder; + late final Border bottomBorder; + late final Border diagonalBorder; + late final bool diagonalBorderUp; + late final bool diagonalBorderDown; + + _BorderSet({ + required this.leftBorder, + required this.rightBorder, + required this.topBorder, + required this.bottomBorder, + required this.diagonalBorder, + required this.diagonalBorderUp, + required this.diagonalBorderDown, + }); + + _BorderSet copyWith({ + Border? leftBorder, + Border? rightBorder, + Border? topBorder, + Border? bottomBorder, + Border? diagonalBorder, + bool? diagonalBorderUp, + bool? diagonalBorderDown, + }) { + return _BorderSet( + leftBorder: leftBorder ?? this.leftBorder, + rightBorder: rightBorder ?? this.rightBorder, + topBorder: topBorder ?? this.topBorder, + bottomBorder: bottomBorder ?? this.bottomBorder, + diagonalBorder: diagonalBorder ?? this.diagonalBorder, + diagonalBorderUp: diagonalBorderUp ?? this.diagonalBorderUp, + diagonalBorderDown: diagonalBorderDown ?? this.diagonalBorderDown, + ); + } + + @override + List get props => [ + leftBorder, + rightBorder, + topBorder, + bottomBorder, + diagonalBorder, + diagonalBorderUp, + diagonalBorderDown, + ]; +} + +enum BorderStyle { + None('none'), + DashDot('dashDot'), + DashDotDot('dashDotDot'), + Dashed('dashed'), + Dotted('dotted'), + Double('double'), + Hair('hair'), + Medium('medium'), + MediumDashDot('mediumDashDot'), + MediumDashDotDot('mediumDashDotDot'), + MediumDashed('mediumDashed'), + SlantDashDot('slantDashDot'), + Thick('thick'), + Thin('thin'); + + final String style; + const BorderStyle(this.style); +} + +BorderStyle? getBorderStyleByName(String name) => + BorderStyle.values.firstWhereOrNull((e) => + e.toString().toLowerCase() == 'borderstyle.' + name.toLowerCase()); diff --git a/lib/src/sheet/cell_style.dart b/lib/src/sheet/cell_style.dart index 201f517b..aa4cb691 100644 --- a/lib/src/sheet/cell_style.dart +++ b/lib/src/sheet/cell_style.dart @@ -12,6 +12,13 @@ class CellStyle extends Equatable { Underline _underline = Underline.None; int? _fontSize; int _rotation = 0; + late Border _leftBorder; + late Border _rightBorder; + late Border _topBorder; + late Border _bottomBorder; + late Border _diagonalBorder; + bool _diagonalBorderUp = false; + bool _diagonalBorderDown = false; CellStyle({ String fontColorHex = 'FF000000', @@ -25,6 +32,13 @@ class CellStyle extends Equatable { Underline underline = Underline.None, bool italic = false, int rotation = 0, + Border? leftBorder, + Border? rightBorder, + Border? topBorder, + Border? bottomBorder, + Border? diagonalBorder, + bool diagonalBorderUp = false, + bool diagonalBorderDown = false, }) { _textWrapping = textWrapping; @@ -45,6 +59,20 @@ class CellStyle extends Equatable { _verticalAlign = verticalAlign; _horizontalAlign = horizontalAlign; + + _leftBorder = leftBorder ?? Border(); + + _rightBorder = rightBorder ?? Border(); + + _topBorder = topBorder ?? Border(); + + _bottomBorder = bottomBorder ?? Border(); + + _diagonalBorder = diagonalBorder ?? Border(); + + _diagonalBorderUp = diagonalBorderUp; + + _diagonalBorderDown = diagonalBorderDown; } CellStyle copyWith({ @@ -59,6 +87,13 @@ class CellStyle extends Equatable { Underline? underlineVal, int? fontSizeVal, int? rotationVal, + Border? leftBorderVal, + Border? rightBorderVal, + Border? topBorderVal, + Border? bottomBorderVal, + Border? diagonalBorderVal, + bool? diagonalBorderUpVal, + bool? diagonalBorderDownVal, }) { return CellStyle( fontColorHex: fontColorHexVal ?? this._fontColorHex, @@ -72,6 +107,13 @@ class CellStyle extends Equatable { underline: underlineVal ?? this._underline, fontSize: fontSizeVal ?? this._fontSize, rotation: rotationVal ?? this._rotation, + leftBorder: leftBorderVal ?? this._leftBorder, + rightBorder: rightBorderVal ?? this._rightBorder, + topBorder: topBorderVal ?? this._topBorder, + bottomBorder: bottomBorderVal ?? this._bottomBorder, + diagonalBorder: diagonalBorderVal ?? this._diagonalBorder, + diagonalBorderUp: diagonalBorderUpVal ?? this._diagonalBorderUp, + diagonalBorderDown: diagonalBorderDownVal ?? this._diagonalBorderDown, ); } @@ -215,6 +257,90 @@ class CellStyle extends Equatable { _italic = italic; } + ///Get `LeftBorder` + /// + Border get leftBorder { + return _leftBorder; + } + + ///Set `LeftBorder` + /// + set leftBorder(Border? leftBorder) { + _leftBorder = leftBorder ?? Border(); + } + + ///Get `RightBorder` + /// + Border get rightBorder { + return _rightBorder; + } + + ///Set `RightBorder` + /// + set rightBorder(Border? rightBorder) { + _rightBorder = rightBorder ?? Border(); + } + + ///Get `TopBorder` + /// + Border get topBorder { + return _topBorder; + } + + ///Set `TopBorder` + /// + set topBorder(Border? topBorder) { + _topBorder = topBorder ?? Border(); + } + + ///Get `BottomBorder` + /// + Border get bottomBorder { + return _bottomBorder; + } + + ///Set `BottomBorder` + /// + set bottomBorder(Border? bottomBorder) { + _bottomBorder = bottomBorder ?? Border(); + } + + ///Get `DiagonalBorder` + /// + Border get diagonalBorder { + return _diagonalBorder; + } + + ///Set `DiagonalBorder` + /// + set diagonalBorder(Border? diagonalBorder) { + _diagonalBorder = diagonalBorder ?? Border(); + } + + ///Get `DiagonalBorderUp` + /// + bool get diagonalBorderUp { + return _diagonalBorderUp; + } + + ///Set `DiagonalBorderUp` + /// + set diagonalBorderUp(bool diagonalBorderUp) { + _diagonalBorderUp = diagonalBorderUp; + } + + ///Get `DiagonalBorderDown` + /// + bool get diagonalBorderDown { + return _diagonalBorderDown; + } + + ///Set `DiagonalBorderDown` + /// + set diagonalBorderDown(bool diagonalBorderDown) { + _diagonalBorderDown = diagonalBorderDown; + } + @override List get props => [ _bold, @@ -228,5 +354,12 @@ class CellStyle extends Equatable { _horizontalAlign, _fontColorHex, _backgroundColorHex, + _leftBorder, + _rightBorder, + _topBorder, + _bottomBorder, + _diagonalBorder, + _diagonalBorderUp, + _diagonalBorderDown, ]; } diff --git a/lib/src/sheet/header_footer.dart b/lib/src/sheet/header_footer.dart index 7f2089e8..09551428 100644 --- a/lib/src/sheet/header_footer.dart +++ b/lib/src/sheet/header_footer.dart @@ -1,163 +1,74 @@ part of excel; class HeaderFooter { - late bool? _alignWithMargins; - late bool? _differentFirst; - late bool? _differentOddEven; - late bool? _scaleWithDoc; - - late String? _evenFooter; - late String? _evenHeader; - late String? _firstFooter; - late String? _firstHeader; - late String? _oddFooter; - late String? _oddHeader; + bool? alignWithMargins; + bool? differentFirst; + bool? differentOddEven; + bool? scaleWithDoc; + + String? evenFooter; + String? evenHeader; + String? firstFooter; + String? firstHeader; + String? oddFooter; + String? oddHeader; HeaderFooter({ - bool? alignWithMargins = null, - bool? differentFirst = null, - bool? differentOddEven = null, - bool? scaleWithDoc = null, - String? evenFooter = null, - String? evenHeader = null, - String? firstFooter = null, - String? firstHeader = null, - String? oddFooter = null, - String? oddHeader = null, - }) : _alignWithMargins = alignWithMargins, - _differentFirst = differentFirst, - _differentOddEven = differentOddEven, - _scaleWithDoc = scaleWithDoc, - _evenFooter = evenFooter, - _evenHeader = evenHeader, - _firstFooter = firstFooter, - _firstHeader = firstHeader, - _oddFooter = oddFooter, - _oddHeader = oddHeader; - - bool? get alignWithMargins { - return _alignWithMargins; - } - - set alignWithMargins(bool? alignWithMargins) { - _alignWithMargins = alignWithMargins; - } - - bool? get differentFirst { - return _differentFirst; - } - - set differentFist(bool? differentFirst) { - _differentFirst = differentFirst; - } - - bool? get differentOddEven { - return _differentOddEven; - } - - set differentOddEven(bool? differentOddEven) { - _differentOddEven = differentOddEven; - } - - bool? get scaleWithDoc { - return _scaleWithDoc; - } - - set scaleWithDoc(bool? scaleWithDoc) { - _scaleWithDoc = scaleWithDoc; - } - - String? get evenFooter { - return _evenFooter; - } - - set evenFooter(String? evenFooter) { - _evenFooter = evenFooter; - } - - String? get evenHeader { - return _evenHeader; - } - - set evenHeader(String? evenHeader) { - _evenHeader = evenHeader; - } - - String? get firstFooter { - return _firstFooter; - } - - set firstFooter(String? firstFooter) { - _firstFooter = firstFooter; - } - - String? get firstHeader { - return _firstHeader; - } - - set firstHeader(String? firstHeader) { - _firstHeader = firstHeader; - } - - String? get oddFooter { - return _oddFooter; - } - - set oddFooter(String? oddFooter) { - _oddFooter = oddFooter; - } - - String? get oddHeader { - return _oddHeader; - } - - set oddHeader(String? oddHeader) { - _oddHeader = oddHeader; - } + this.alignWithMargins, + this.differentFirst, + this.differentOddEven, + this.scaleWithDoc, + this.evenFooter, + this.evenHeader, + this.firstFooter, + this.firstHeader, + this.oddFooter, + this.oddHeader, + }); XmlNode toXmlElement() { final attributes = []; - if (_alignWithMargins != null) { + if (alignWithMargins != null) { attributes.add(XmlAttribute( - XmlName("alignWithMargins"), _alignWithMargins.toString())); + XmlName("alignWithMargins"), alignWithMargins.toString())); } - if (_differentFirst != null) { + if (differentFirst != null) { attributes.add( - XmlAttribute(XmlName("differentFirst"), _differentFirst.toString())); + XmlAttribute(XmlName("differentFirst"), differentFirst.toString())); } - if (_differentOddEven != null) { + if (differentOddEven != null) { attributes.add(XmlAttribute( - XmlName("differentOddEven"), _differentOddEven.toString())); + XmlName("differentOddEven"), differentOddEven.toString())); } - if (_scaleWithDoc != null) { + if (scaleWithDoc != null) { attributes - .add(XmlAttribute(XmlName("scaleWithDoc"), _scaleWithDoc.toString())); + .add(XmlAttribute(XmlName("scaleWithDoc"), scaleWithDoc.toString())); } final children = []; - if (_evenFooter != null) { - children - .add(XmlElement(XmlName("evenFooter"), [], [XmlText(_evenFooter!)])); + if (evenHeader != null) { + children.add(XmlElement( + XmlName("evenHeader"), [], [XmlText(evenHeader!.simplifyText())])); } - if (_evenHeader != null) { - children - .add(XmlElement(XmlName("evenHeader"), [], [XmlText(_evenHeader!)])); + if (evenFooter != null) { + children.add(XmlElement( + XmlName("evenFooter"), [], [XmlText(evenFooter!.simplifyText())])); } - if (_firstFooter != null) { - children.add( - XmlElement(XmlName("firstFooter"), [], [XmlText(_firstFooter!)])); + if (firstHeader != null) { + children.add(XmlElement( + XmlName("firstHeader"), [], [XmlText(firstHeader!.simplifyText())])); } - if (_firstHeader != null) { - children.add( - XmlElement(XmlName("firstHeader"), [], [XmlText(_firstHeader!)])); + if (firstFooter != null) { + children.add(XmlElement( + XmlName("firstFooter"), [], [XmlText(firstFooter!.simplifyText())])); } - if (_oddFooter != null) { - children - .add(XmlElement(XmlName("oddFooter"), [], [XmlText(_oddFooter!)])); + if (oddHeader != null) { + children.add(XmlElement( + XmlName("oddHeader"), [], [XmlText(oddHeader!.simplifyText())])); } - if (_oddHeader != null) { - children - .add(XmlElement(XmlName("oddHeader"), [], [XmlText(_oddHeader!)])); + if (oddFooter != null) { + children.add(XmlElement( + XmlName("oddFooter"), [], [XmlText(oddFooter!.simplifyText())])); } return XmlElement(XmlName("headerFooter"), attributes, children); @@ -173,12 +84,12 @@ class HeaderFooter { headerFooterElement.getAttribute("differentOddEven")?.parseBool(), scaleWithDoc: headerFooterElement.getAttribute("scaleWithDoc")?.parseBool(), - evenFooter: headerFooterElement.getElement("evenFooter")?.innerXml, - evenHeader: headerFooterElement.getElement("evenHeader")?.innerXml, - firstFooter: headerFooterElement.getElement("firstFooter")?.innerXml, - firstHeader: headerFooterElement.getElement("firstHeader")?.innerXml, - oddFooter: headerFooterElement.getElement("oddFooter")?.innerXml, - oddHeader: headerFooterElement.getElement("oddHeader")?.innerXml); + evenHeader: headerFooterElement.getElement("evenHeader")?.text, + evenFooter: headerFooterElement.getElement("evenFooter")?.text, + firstHeader: headerFooterElement.getElement("firstHeader")?.text, + firstFooter: headerFooterElement.getElement("firstFooter")?.text, + oddFooter: headerFooterElement.getElement("oddFooter")?.text, + oddHeader: headerFooterElement.getElement("oddHeader")?.text); } } @@ -193,4 +104,12 @@ extension BoolParsing on String { throw '"$this" can not be parsed to boolean.'; } + + String simplifyText() { + String value = this.replaceAll('&', '&'); + value = value.replaceAll('amp', '&'); + value = value.replaceAll('&', '&'); + value = value.replaceAll('"', '"'); + return value; + } } diff --git a/lib/src/sheet/sheet.dart b/lib/src/sheet/sheet.dart index 2b0a6b3e..93c4f204 100644 --- a/lib/src/sheet/sheet.dart +++ b/lib/src/sheet/sheet.dart @@ -716,16 +716,19 @@ class Sheet { Data value = Data.newData(this, startRow, startColumn); if (customValue != null) { - value._value = customValue; + if (customValue is String) { + final sharedString = _excel._sharedStrings.addFromString(customValue); + value._value = sharedString; + } else { + value._value = customValue; + } getValue = false; } for (int j = startRow; j <= endRow; j++) { for (int k = startColumn; k <= endColumn; k++) { - if (_sheetData[j] != null && _sheetData[j]![k] != null) { - if (getValue && - _sheetData[j]![k]!.value != null && - _sheetData[j]![k]!.cellStyle != null) { + if (_sheetData[j] != null) { + if (getValue && _sheetData[j]![k]?.value != null) { value = _sheetData[j]![k]!; getValue = false; } diff --git a/pubspec.yaml b/pubspec.yaml index 7955c03d..a0871335 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: excel description: A flutter and dart library for reading, creating, editing and updating excel sheets with compatible both on client and server side. -version: 2.0.4 +version: 2.1.0 homepage: https://github.com/justkawal/excel environment: @@ -9,6 +9,7 @@ environment: dependencies: archive: ">=3.1.0 <4.0.0" xml: ">=5.0.0 <7.0.0" + collection: ^1.17.0 equatable: ^2.0.0 dev_dependencies: diff --git a/test/excel_test.dart b/test/excel_test.dart index 178fca2e..2895aeb2 100644 --- a/test/excel_test.dart +++ b/test/excel_test.dart @@ -270,4 +270,121 @@ void main() { expect(headerFooter.scaleWithDoc, isFalse); }); }); + + group('Borders', () { + test('read file with borders', () { + final file = './test/test_resources/borders.xlsx'; + final bytes = File(file).readAsBytesSync(); + final excel = Excel.decodeBytes(bytes); + final Sheet sheetObject = excel.tables['Sheet1']!; + + final borderEmpty = Border(); + final borderMedium = Border(borderStyle: BorderStyle.Medium); + final borderMediumRed = + Border(borderStyle: BorderStyle.Medium, borderColorHex: 'FFFF0000'); + final borderHair = Border(borderStyle: BorderStyle.Hair); + final borderDouble = Border(borderStyle: BorderStyle.Double); + + final cellStyleA1 = + sheetObject.cell(CellIndex.indexByString('A1')).cellStyle; + expect(cellStyleA1?.leftBorder, equals(borderMedium)); + expect(cellStyleA1?.rightBorder, equals(borderMedium)); + expect(cellStyleA1?.topBorder, anyOf(isNull, equals(borderEmpty))); + expect(cellStyleA1?.bottomBorder, equals(borderMediumRed)); + expect(cellStyleA1?.diagonalBorder, anyOf(isNull, equals(borderEmpty))); + expect(cellStyleA1?.diagonalBorderUp, isFalse); + expect(cellStyleA1?.diagonalBorderDown, isFalse); + + final cellStyleB3 = + sheetObject.cell(CellIndex.indexByString('B3')).cellStyle; + expect(cellStyleB3?.leftBorder, equals(borderMedium)); + expect(cellStyleB3?.rightBorder, equals(borderMedium)); + expect(cellStyleB3?.topBorder, equals(borderHair)); + expect(cellStyleB3?.bottomBorder, equals(borderHair)); + + final cellStyleA5 = + sheetObject.cell(CellIndex.indexByString('A5')).cellStyle; + expect(cellStyleA5?.diagonalBorder, equals(borderDouble)); + expect(cellStyleA5?.diagonalBorderUp, isFalse); + expect(cellStyleA5?.diagonalBorderDown, isTrue); + + final cellStyleC5 = + sheetObject.cell(CellIndex.indexByString('C5')).cellStyle; + expect(cellStyleC5?.diagonalBorder, equals(borderDouble)); + expect(cellStyleC5?.diagonalBorderUp, isTrue); + expect(cellStyleC5?.diagonalBorderDown, isFalse); + }); + + test('test support all border styles', () { + final file = './test/test_resources/borders2.xlsx'; + final bytes = File(file).readAsBytesSync(); + final excel = Excel.decodeBytes(bytes); + final Sheet sheetObject = excel.tables['Sheet1']!; + + final borderStyles = [ + BorderStyle.None, + BorderStyle.DashDot, + BorderStyle.DashDotDot, + BorderStyle.Dashed, + BorderStyle.Dotted, + BorderStyle.Double, + BorderStyle.Hair, + BorderStyle.Medium, + BorderStyle.MediumDashDot, + BorderStyle.MediumDashDotDot, + BorderStyle.MediumDashed, + BorderStyle.SlantDashDot, + BorderStyle.Thick, + BorderStyle.Thin, + ]; + + for (var i = 1; i < borderStyles.length; ++i) { + // Loop from i = 1, as Excel does not set None type. + final border = Border(borderStyle: borderStyles[i]); + + final cellStyle = sheetObject + .cell(CellIndex.indexByString('B${2 * (i + 1)}')) + .cellStyle; + + expect(cellStyle?.leftBorder, equals(border)); + expect(cellStyle?.rightBorder, equals(border)); + expect(cellStyle?.topBorder, equals(border)); + expect(cellStyle?.bottomBorder, equals(border)); + } + }); + + test('saving XLSX File with borders', () async { + final file = './test/test_resources/borders.xlsx'; + final bytes = File(file).readAsBytesSync(); + final excel = Excel.decodeBytes(bytes); + + final outFilePath = Directory.current.path + '/tmp/bordersOut.xlsx'; + final fileBytes = excel.encode(); + if (fileBytes != null) { + File(outFilePath) + ..createSync(recursive: true) + ..writeAsBytesSync(fileBytes); + } + + final newFileBytes = File(outFilePath).readAsBytesSync(); + final newExcel = Excel.decodeBytes(newFileBytes); + expect(newExcel.sheets.entries.length, equals(1)); + + final borderEmpty = Border(); + final borderMedium = Border(borderStyle: BorderStyle.Medium); + final borderMediumRed = + Border(borderStyle: BorderStyle.Medium, borderColorHex: 'FFFF0000'); + + final Sheet sheetObject = newExcel.tables['Sheet1']!; + final cellStyleB1 = + sheetObject.cell(CellIndex.indexByString('B1')).cellStyle; + expect(cellStyleB1?.leftBorder, equals(borderMedium)); + expect(cellStyleB1?.rightBorder, equals(borderMedium)); + expect(cellStyleB1?.topBorder, equals(borderEmpty)); + expect(cellStyleB1?.bottomBorder, equals(borderMediumRed)); + + // delete tmp folder only when test is successful (diagnosis) + new Directory('./tmp').delete(recursive: true); + }); + }); } diff --git a/test/test_resources/borders.xlsx b/test/test_resources/borders.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..62db2b3904e75a758b119a20b9153ac8054de350 GIT binary patch literal 10822 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 literal 0 HcmV?d00001